R4 the client catches up: follow requests, followers, downvotes, dismissing, filtering a post, quote policies, statistics in detail
Build / Build (push) Successful in 1m8s

Follow requests answered on the persona's own profile and from the Notifications tab; its followers and the accounts it
follows listed there, a follower removed (asked first). Downvotes on threadiverse posts, after the public-vote notice.
A notification dismissed from its line. "Filter this post" from a card's menu, and the Filters page showing the posts a
filter holds. Who may quote the persona's own posts, and revoking a quote of one. Statistics: a server in detail with
"describe again", the ledger's events, the server's own days. The composer deletes an upload taken out before posting.
MediaType.Documents goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-17 04:02:20 +02:00
1 parent b731d8c8c0
commit b8d7858acb
25 files changed
+1543 -61

No files matched your search

+164 -4
View File
@@ -1,7 +1,10 @@
@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) and how far the crawler got. Counted per server, never per account. *@
(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. *@
<div class="flex flex-col gap-4 w-full" data-statistics>
<div class="flex flex-wrap gap-2 items-end">
<label class="flex flex-col gap-1 text-sm">
@@ -87,7 +90,10 @@
@foreach (var host in hosts.Hosts)
{
<tr data-statistics-host="@host.Host">
<td class="max-w-48 truncate">@host.Host</td>
<td class="max-w-48">
<button type="button" class="link link-hover block max-w-full truncate text-left pointer-coarse:py-3.5" title="@host.Host"
@onclick="() => openHost = host.Host" data-statistics-host-open>@host.Host</button>
</td>
<td>@host.Software @host.Version</td>
<td class="text-right tabular-nums">@host.In</td>
<td class="text-right tabular-nums">@host.Out</td>
@@ -116,6 +122,11 @@
<p class="text-xs opacity-70">@hosts.Attribution</p>
}
@if (openHost is { } detailHost)
{
<HostDetailDialog HostName="@detailHost" Days="days" OnClosed="() => openHost = null" />
}
@if (crawler is not null)
{
<div class="flex flex-col gap-1 text-xs" data-statistics-crawler>
@@ -141,6 +152,99 @@
}
</div>
}
<div class="flex flex-col gap-2" data-statistics-events>
<b class="text-sm">@Localizer["Events"]</b>
<div class="flex flex-wrap gap-2 items-end">
<label class="flex flex-col gap-1 text-sm">
<span>@Localizer["server"]</span>
<DInput TValue="string" Size="DSize.Sm" @bind-Value="eventsHost" @bind-Value:after="ReloadEvents" autocomplete="off" maxlength="253"
placeholder="@Localizer["A server, e.g. example.social"]" data-statistics-events-host />
</label>
<label class="flex flex-col gap-1 text-sm">
<span>@Localizer["Channel"]</span>
<DSelect TValue="string" Size="DSize.Sm" Value="eventsChannel" ValueChanged="ChangeChannel" ValueExpression="() => eventsChannel"
data-statistics-events-channel>
<option value="">@Localizer["All"]</option>
@foreach (var channel in EventChannels)
{
<option value="@channel">@channel</option>
}
</DSelect>
</label>
<label class="flex flex-col gap-1 text-sm">
<span>@Localizer["Outcome"]</span>
<DSelect TValue="string" Size="DSize.Sm" Value="eventsOutcome" ValueChanged="ChangeOutcome" ValueExpression="() => eventsOutcome"
data-statistics-events-outcome>
<option value="">@Localizer["All"]</option>
@foreach (var outcome in EventOutcomes)
{
<option value="@outcome">@outcome</option>
}
</DSelect>
</label>
</div>
@if (events.Count == 0)
{
<p class="text-xs opacity-70" data-statistics-events-empty>@Localizer["Empty"]</p>
}
else
{
<EventsTable Events="events" />
}
@if (hasMoreEvents)
{
<DButton Size="DSize.Sm" class="self-center" Disabled="@(IsDefaultDisabled || isLoadingMore)" OnClick="MoreEvents" data-statistics-events-more>
@if (isLoadingMore)
{
<DLoading Size="DSize.Xs" LabelText="@Localizer["loading"]" />
}
<span>@Localizer["Load more"]</span>
</DButton>
}
</div>
<div class="flex flex-col gap-2" data-statistics-server>
<b class="text-sm">@Localizer["Server days"]</b>
@if (serverDays.Count == 0)
{
<p class="text-xs opacity-70">@Localizer["Empty"]</p>
}
else
{
<DTable Size="DSize.Xs" Zebra="true" WrapperClass="max-h-[28rem] rounded-xl neo-inset neo-2xs" data-scroll-x>
<thead>
<tr>
<th>@Localizer["Day"]</th>
<th class="text-right">@Localizer["Client API"]</th>
<th class="text-right">4xx</th>
<th class="text-right">5xx</th>
<th class="text-right">@Localizer["Served"]</th>
<th class="text-right" title="@Localizer["written / dropped / failed"]">@Localizer["Ledger"]</th>
<th class="text-right">@Localizer["Accounts"]</th>
<th class="text-right"><span class="capitalize">@Localizer["servers"]</span></th>
</tr>
</thead>
<tbody>
@foreach (var day in Enumerable.Reverse(serverDays))
{
<tr data-statistics-server-day="@NerdRow.Time(day.Day)">
<td class="whitespace-nowrap">@day.Day.ToString(SUtility.dateTimeFormat)</td>
<td class="text-right tabular-nums">@ClientRequests(day, null)</td>
<td class="text-right tabular-nums">@ClientRequests(day, ":4xx")</td>
<td class="text-right tabular-nums">@ClientRequests(day, ":5xx")</td>
<td class="text-right tabular-nums">@day.Served.Values.Sum()</td>
<td class="text-right tabular-nums whitespace-nowrap" title="@Localizer["written / dropped / failed"]">
@day.LedgerWritten / @day.LedgerDropped / @day.LedgerFailed
</td>
<td class="text-right tabular-nums">@day.Accounts</td>
<td class="text-right tabular-nums">@day.Hosts</td>
</tr>
}
</tbody>
</DTable>
}
</div>
}
</div>
@@ -148,11 +252,24 @@
[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<ViewInteractionEvent> events = [];
List<ViewServerDay> 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;
@@ -179,10 +296,53 @@
}
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);
crawler = await AdminService.Crawler();
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() ?? "—";
@@ -191,7 +351,7 @@
{
days = value;
page = 1;
await LoadData();
await LoadDays();
}
async Task ChangeSort(string value)