R5 the server's own settings in decePub: instance, rules, announcements, relays, media limits, the staff's log, About
Build / Build (push) Successful in 1m5s

Administration's Pleroma placeholders give way to real sections: Instance (title, descriptions, contact, thumbnail,
privacy policy, terms, the weekly activity), Rules, Announcements, Relays, Media limits and the Moderation log, next to
Reports, blocked servers (now published or obfuscated), statistics and backups; moderators see Reports and the log. Every
save sends the whole settings form built from the latest read, since PrivaPub replaces what it carries. /about shows the
server's documents to anyone. The report dialog picks the rules a violation breaks, and reports show them. Announcements
show above the feed tabs, dismissed and reacted to, live from the user stream. Login names the server.

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:32:57 +02:00
1 parent b8d7858acb
commit 4c88980374
33 files changed
+3743 -239

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
@inherits LocalizableComponentBase
@* The staff's log (/clientapi/moderator/log), newest first, 50 at a time: who acted (to administrators every login's
name; to a moderator only its own; "the command line" for PrivaPub's CLI; nobody when an act on a persona of the
acting login would tie the two), what was done, to what, and its note. Never an IP address. A moderator who isn't an
administrator reads what isn't about a login. *@
<div class="flex flex-col gap-2 w-full" data-log>
<div class="flex flex-wrap items-center gap-2">
<DButton Size="DSize.Sm" Disabled="IsDefaultDisabled || isLoadingMore" OnClick="Reload" data-log-reload>
<i class="ion-md-refresh" aria-hidden="true"></i><span>@Localizer["Read again"]</span>
</DButton>
</div>
@if (IsLoading)
{
<p class="flex w-full items-center justify-center gap-2">
<DLoading Variant="DLoadingVariant.Dots" LabelText="@Localizer["loading"]" />
@Localizer["loading"]
</p>
}
else if (failed)
{
<p class="text-sm text-error">@Localizer["The log could not be read."]</p>
}
else if (entries.Count == 0)
{
<p class="text-sm opacity-70" data-log-empty>@Localizer["Nothing was done yet."]</p>
}
<ol class="flex flex-col gap-2">
@foreach (var entry in entries)
{
<li class="flex flex-col gap-1 p-2 rounded-lg neo-raised neo-2xs" data-log-entry="@entry.Id" data-log-action="@entry.Action">
<div class="flex flex-wrap items-center gap-2">
<b class="text-sm">@ActionName(entry.Action)</b>
@if (entry.TargetLabel is { Length: > 0 } label)
{
<span class="text-sm break-all min-w-0" title="@($"{entry.TargetKind} {entry.TargetId}")" data-log-target>@label</span>
}
<span class="flex-1"></span>
<span class="text-xs opacity-70" title="@entry.At.ToLocalTime().ToString(SUtility.dateTimeFormatExt)">@entry.At.GetPassedTime(Localizer)</span>
</div>
<div class="flex flex-wrap items-center gap-2 text-xs">
<span data-log-by="@(entry.ByYou ? "you" : entry.By ?? "none")">@By(entry)</span>
<code class="opacity-60">@entry.Action</code>
@if (entry.ReportId is { Length: > 0 } reportId)
{
<span class="opacity-70">@Localizer["Report: {0}", reportId]</span>
}
</div>
@if (entry.Note is { Length: > 0 } note)
{
<p class="text-xs opacity-80 break-words" data-log-note>@note</p>
}
</li>
}
</ol>
@if (next is not null)
{
<DButton Size="DSize.Sm" class="self-center" Disabled="IsDefaultDisabled || isLoadingMore" OnClick="More" data-log-more>
@if (isLoadingMore)
{
<DLoading Size="DSize.Sm" LabelText="@Localizer["loading"]" />
}
<span>@Localizer["Load more"]</span>
</DButton>
}
</div>
@code {
const int PageSize = 50;
[Inject] AdminService AdminService { get; set; }
List<ViewAdminAction> entries = [];
// the next page's `before`; null on the last
string next;
bool failed;
bool isLoadingMore;
protected override async Task OnInitializedAsync()
{
if (CascadingState.IsOffline)
{
WhenOnline(LoadData);
StartOnlinePolling();
IsLoading = false;
return;
}
await LoadData();
IsLoading = false;
}
async Task LoadData()
{
var page = await AdminService.Log(null, PageSize);
failed = page is null;
entries = page?.Items ?? [];
next = page?.Next;
}
async Task Reload()
{
IsLoading = true;
await LoadData();
IsLoading = false;
}
async Task More()
{
isLoadingMore = true;
var page = await AdminService.Log(next, PageSize);
isLoadingMore = false;
if (page is null)
{
Toast.AddResponseError(Localizer["The log could not be read."]);
return;
}
var known = entries.Select(e => e.Id).ToHashSet();
entries.AddRange(page.Items.Where(e => known.Add(e.Id)));
next = page.Next;
}
string By(ViewAdminAction entry) => entry switch
{
{ ByYou: true } => Localizer["by you"],
{ By: "cli" } => Localizer["by the command line"],
{ By: { Length: > 0 } name } => Localizer["by {0}", name],
_ => Localizer["by someone not named"]
};
string ActionName(string action) => action switch
{
"domain_block.insert" => Localizer["Server blocked"],
"domain_block.delete" => Localizer["Server unblocked"],
"backup.create" => Localizer["Backup made"],
"backup.delete" => Localizer["Backup deleted"],
"backup.restore" => Localizer["Backup restored"],
"settings.update" => Localizer["Settings saved"],
"settings.thumbnail" => Localizer["Thumbnail changed"],
"rule.insert" => Localizer["Rule added"],
"rule.update" => Localizer["Rule changed"],
"rule.delete" => Localizer["Rule removed"],
"announcement.insert" => Localizer["Announcement written"],
"announcement.update" => Localizer["Announcement changed"],
"announcement.delete" => Localizer["Announcement deleted"],
"relay.insert" => Localizer["Relay added"],
"relay.delete" => Localizer["Relay removed"],
"report.resolve" => Localizer["Report resolved"],
"root.promote" => Localizer["Login made administrator"],
"root.demote" => Localizer["Login no longer administrator"],
"root.create" => Localizer["Login made"],
"root.delete" => Localizer["Login deleted"],
"root.reset_totp" => Localizer["Two-factor sign-in reset"],
"persona.move" => Localizer["Persona moved to another login"],
_ => action
};
}