Show deleted entity name beside target id in admin Audit Log

Task #133: Admin Audit Log rows now display the deleted item's
human-readable name next to its `<type> #<id>` target reference
without admins having to expand the JSON metadata.

Changes
- artifacts/tx-os/src/pages/admin.tsx
  - New helper `forceDeletedEntityName(entry, lang)` that pulls a
    localized display name from a force-delete row's metadata.
    Priority: nameEn/nameAr/displayNameEn/displayNameAr (lang-aware)
    > plain `name` > `@username`.
  - `AuditLogRow` renders an additional muted "Target: <type> #<id>
    · <name>" line beneath the summary for force-delete entries that
    expose a name. When no summary is generated, the existing target
    fallback line is upgraded to include the name as well.
- artifacts/tx-os/src/locales/en.json, ar.json
  - New `admin.audit.targetWithName` key with English and Arabic
    translations.

Behavior
- Names respect the active language (Arabic preferred when lang=ar,
  with graceful fallback to the other locale, then `name`, then
  `@username`).
- Non-deletion rows and deletion rows whose metadata lacks a name
  are unchanged — no extra line is rendered, so there is no
  regression for existing entries.

Verification
- `pnpm exec tsc --noEmit` in artifacts/tx-os passes after running
  `pnpm --filter @workspace/api-spec run codegen`.
- E2E test (Playwright) confirms the new line renders for both a
  service.force_delete row (English-only metadata) and an
  app.delete force row (English + Arabic metadata), and that the
  Arabic UI surfaces the Arabic name when present and falls back to
  the English name when not.

Follow-ups proposed
- #194 Persist user display names in user.delete audit metadata.
- #195 Log every service deletion, not only forced ones.

Replit-Task-Id: cd312541-1c90-4849-afd6-e6757aedfe06
This commit is contained in:
riyadhafraa
2026-04-30 06:52:21 +00:00
parent 9a0935f58e
commit 0062430b31
4 changed files with 45 additions and 2 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

+1
View File
@@ -561,6 +561,7 @@
"forcedOnlyHint": "عرض إدخالات الحذف القسري للمجموعات والمستخدمين والتطبيقات والخدمات فقط."
},
"target": "الهدف: {{type}} #{{id}}",
"targetWithName": "الهدف: {{type}} #{{id}} · {{name}}",
"showing": "عرض {{shown}} من أصل {{total}}",
"empty": "لا توجد إدخالات في سجل التدقيق تطابق الفلاتر.",
"loadMore": "عرض المزيد",
+1
View File
@@ -546,6 +546,7 @@
"forcedOnlyHint": "Show only force-delete entries for groups, users, apps, and services."
},
"target": "Target: {{type}} #{{id}}",
"targetWithName": "Target: {{type}} #{{id}} · {{name}}",
"showing": "Showing {{shown}} of {{total}}",
"empty": "No audit log entries match these filters.",
"loadMore": "Load more",
+43 -2
View File
@@ -4837,6 +4837,27 @@ function isForceDeleteEntry(entry: AuditLogEntry): boolean {
return false;
}
// Pulls a human-readable display name for a force-deleted entity from its
// audit metadata so the row can show "service #4821 · My Service" inline
// without admins having to expand the JSON. Returns null when the row is not
// a forced deletion or the metadata lacks any usable name field.
function forceDeletedEntityName(
entry: AuditLogEntry,
lang: string,
): string | null {
if (!isForceDeleteEntry(entry)) return null;
const meta = asRecord(entry.metadata);
const en = asString(meta.nameEn) ?? asString(meta.displayNameEn);
const ar = asString(meta.nameAr) ?? asString(meta.displayNameAr);
const localized = lang === "ar" ? ar ?? en : en ?? ar;
if (localized) return localized;
const plain = asString(meta.name);
if (plain) return plain;
const username = asString(meta.username);
if (username) return `@${username}`;
return null;
}
function dependencyChips(
entry: AuditLogEntry,
t: AuditTFunction,
@@ -4861,6 +4882,8 @@ function AuditLogRow({ entry, lang }: { entry: AuditLogEntry; lang: string }) {
const summary = formatAuditSummary(entry, t, lang);
const isForced = isForceDeleteEntry(entry);
const chips = isForced ? dependencyChips(entry, t) : [];
const deletedName = forceDeletedEntityName(entry, lang);
const showDeletedTarget = deletedName != null && entry.targetId != null;
return (
<div
className="glass-panel rounded-2xl p-3 space-y-2"
@@ -4892,9 +4915,27 @@ function AuditLogRow({ entry, lang }: { entry: AuditLogEntry; lang: string }) {
</div>
) : (
<div className="text-xs text-muted-foreground mt-1">
{t("admin.audit.target", {
{showDeletedTarget
? t("admin.audit.targetWithName", {
type: entry.targetType,
id: entry.targetId,
name: deletedName,
})
: t("admin.audit.target", {
type: entry.targetType,
id: entry.targetId ?? "—",
})}
</div>
)}
{summary && showDeletedTarget && (
<div
className="text-xs text-muted-foreground mt-0.5"
data-testid={`audit-deleted-target-${entry.id}`}
>
{t("admin.audit.targetWithName", {
type: entry.targetType,
id: entry.targetId ?? "—",
id: entry.targetId,
name: deletedName,
})}
</div>
)}