diff --git a/src/app/p/[secret]/log/page.tsx b/src/app/p/[secret]/log/page.tsx index 7c36c78..3f98afd 100644 --- a/src/app/p/[secret]/log/page.tsx +++ b/src/app/p/[secret]/log/page.tsx @@ -51,7 +51,7 @@ const CATEGORIES = Object.keys(AUDIT_CATEGORY_LABEL) as AuditCategory[]; function EntryRow({ entry }: { entry: Entry }) { const [isOpen, setIsOpen] = useState(false); const isSelf = entry.targetType === "USER" && entry.targetId === entry.moderatorId; - const view = formatAuditEntry({ ...entry, targetLabel: isSelf ? "sich selbst" : entry.targetLabel }); + const view = formatAuditEntry({ ...entry, isSelf }); const Icon = entry.isAutomatic ? Bot : CATEGORY_ICON[view.category]; const removal = isRemoval(entry.action); const actor = entry.isAutomatic ? "Automatik" : (entry.moderatorName ?? "Unbekannt"); diff --git a/src/lib/admin/audit-format.test.ts b/src/lib/admin/audit-format.test.ts index 9ef4cc3..9de999f 100644 --- a/src/lib/admin/audit-format.test.ts +++ b/src/lib/admin/audit-format.test.ts @@ -43,6 +43,14 @@ describe("formatAuditEntry", () => { expect(text(fmt("GRANT_ROLE", "MODERATOR", "Lea").parts)).toBe("hat **Lea** die Rolle **Moderator** gegeben"); }); + it("phrases actions on the moderator's own account naturally", () => { + const self = (action: string, reason: string | null = null) => + text(formatAuditEntry({ action, reason, targetLabel: "Daniel", rankName: "Pfoten-Legende", isSelf: true }).parts); + expect(self("DATA_EXPORT")).toBe("hat die eigenen Daten exportiert"); + expect(self("GRANT_REFERRAL_RANK", "rank-1")).toBe("hat sich selbst den Rang **Pfoten-Legende** verliehen"); + expect(self("UNBAN_USER")).toBe("hat die eigene Sperre aufgehoben"); + }); + it("never shows a bare code for unknown actions", () => { const v = fmt("SOMETHING_NEW"); expect(v.category).toBe("OTHER"); diff --git a/src/lib/admin/audit-format.ts b/src/lib/admin/audit-format.ts index 2d21591..6b95788 100644 --- a/src/lib/admin/audit-format.ts +++ b/src/lib/admin/audit-format.ts @@ -67,6 +67,8 @@ export interface AuditInput { targetLabel: string | null; /** Vom Server aufgelöster Rangname bei Empfehlungs-Aktionen. */ rankName?: string | null; + /** Halter-Aktion am eigenen Konto → „sich selbst“ bzw. „die eigenen Daten“ statt zweimal derselbe Name. */ + isSelf?: boolean; } export interface AuditView { @@ -107,13 +109,15 @@ function splitPrefix(reason: string | null): [string | null, string | null] { const reasonLine = (text: string | null | undefined) => (text && text.trim() ? [`Grund: ${text.trim()}`] : []); const formatValue = (v: unknown) => (v === true ? "an" : v === false ? "aus" : String(v)); -export function formatAuditEntry({ action, reason, targetLabel, rankName }: AuditInput): AuditView { +export function formatAuditEntry({ action, reason, targetLabel, rankName, isSelf = false }: AuditInput): AuditView { const category = CATEGORY_BY_ACTION.get(action) ?? "OTHER"; const s = (label: string | null | undefined, fallback: string): AuditPart => (label ? { strong: label } : fallback); const view = (parts: AuditPart[], details: string[] = []): AuditView => ({ category, parts, details }); const post = targetLabel ? ["den Beitrag von ", { strong: targetLabel }] : ["einen Beitrag"]; - const owner = s(targetLabel, "einen Halter"); + const owner: AuditPart = isSelf ? "sich selbst" : s(targetLabel, "einen Halter"); + /** „die Sperre von X“ bzw. am eigenen Konto „die eigene Sperre“. */ + const ofOwner = (noun: string, ownNoun: string): AuditPart[] => (isSelf ? [ownNoun] : [`${noun} von `, owner]); const pet = s(targetLabel, "ein Tier"); const rank = s(rankName, "einen Rang"); @@ -142,17 +146,17 @@ export function formatAuditEntry({ action, reason, targetLabel, rankName }: Audi case "BAN_USER": return view(["hat ", owner, " gesperrt"], reasonLine(reason)); case "UNBAN_USER": - return view(["hat die Sperre von ", owner, " aufgehoben"]); + return view(["hat ", ...ofOwner("die Sperre", "die eigene Sperre"), " aufgehoben"]); case "SHADOWBAN_USER": return view(["hat ", owner, " unsichtbar geschaltet (Shadowban)"], reasonLine(reason)); case "UNSHADOWBAN_USER": - return view(["hat den Shadowban von ", owner, " aufgehoben"]); + return view(["hat ", ...ofOwner("den Shadowban", "den eigenen Shadowban"), " aufgehoben"]); case "WARN_USER": return view(["hat ", owner, " verwarnt"], reasonLine(reason)); case "DELETE_ACCOUNT": - return view(["hat das Konto von ", owner, " gelöscht"], reasonLine(reason)); + return view(["hat ", ...ofOwner("das Konto", "das eigene Konto"), " gelöscht"], reasonLine(reason)); case "DATA_EXPORT": - return view(["hat die Daten von ", owner, " exportiert"]); + return view(["hat ", ...ofOwner("die Daten", "die eigenen Daten"), " exportiert"]); // Tiere case "VERIFY_PET": @@ -241,9 +245,9 @@ export function formatAuditEntry({ action, reason, targetLabel, rankName }: Audi return view(["hat die Einstellungen des ", { strong: "Empfehlungsprogramms" }, " geändert"], details); } case "APPROVE_REFERRAL": - return view(["hat die Einladung von ", owner, " freigegeben"]); + return view(["hat ", ...ofOwner("die Einladung", "die eigene Einladung"), " freigegeben"]); case "REVOKE_REFERRAL": - return view(["hat die Einladung von ", owner, " aberkannt"], reasonLine(reason)); + return view(["hat ", ...ofOwner("die Einladung", "die eigene Einladung"), " aberkannt"], reasonLine(reason)); case "GRANT_REFERRAL_RANK": return view(["hat ", owner, " den Rang ", rank, " verliehen"], splitPrefix(reason)[1] ? [`Notiz: ${splitPrefix(reason)[1]}`] : []); case "RESTORE_REFERRAL_RANK":