fix(admin): translate native-token audit actions, link recent rows

Two related improvements to the admin recent-activity surface:

- New audit actions `auth.token.autoissue.native`,
  `auth.token.refresh`, and `auth.token.revoke` were leaking
  through both the dashboard preview and the Login Overview
  viewer as raw event keys. Added translations and wired the
  three keys through the action-label map shared by both
  components.
- Recent-activity rows on /admin were a dead-end: a failed
  login row showed "Unknown" + "Failed" with no link, no IP,
  no way to drill in. Each row is now a Link into
  /admin/login-overview, with the IP shown inline at sm+.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 13:55:34 +02:00
1 parent 876e074fd8
commit bc81fd7db2
4 files changed
+45 -23

No files matched your search

+3
View File
@@ -1737,6 +1737,9 @@
"authLogout": "Abmeldung",
"authPasskeyRegister": "Passkey hinzugefügt",
"authPasskeyDelete": "Passkey gelöscht",
"authTokenAutoissueNative": "Native-Client-Token ausgestellt",
"authTokenRefresh": "Token erneuert",
"authTokenRevoke": "Token widerrufen",
"medicationReminders": "Medikamentenerinnerungen",
"medicationRemindersDescription": "Schwellenwerte für die Farbübergänge der Erinnerungs-Badges auf den Medikamentenkarten.",
"reminderLateMinutes": "Spät-Schwellenwert (Minuten)",
+3
View File
@@ -1737,6 +1737,9 @@
"authLogout": "Logout",
"authPasskeyRegister": "Passkey added",
"authPasskeyDelete": "Passkey deleted",
"authTokenAutoissueNative": "Native client token issued",
"authTokenRefresh": "Token refreshed",
"authTokenRevoke": "Token revoked",
"medicationReminders": "Medication Reminders",
"medicationRemindersDescription": "Thresholds for reminder badge color transitions on medication cards.",
"reminderLateMinutes": "Late threshold (minutes)",
@@ -76,6 +76,9 @@ export function LoginOverviewSection() {
"auth.logout": t("admin.authLogout"),
"auth.passkey.register": t("admin.authPasskeyRegister"),
"auth.passkey.delete": t("admin.authPasskeyDelete"),
"auth.token.autoissue.native": t("admin.authTokenAutoissueNative"),
"auth.token.refresh": t("admin.authTokenRefresh"),
"auth.token.revoke": t("admin.authTokenRevoke"),
};
// Build the query string once so it's reused by the data-query key, the
+36 -23
View File
@@ -56,6 +56,9 @@ export function RecentAuditPreview() {
"auth.logout": t("admin.authLogout"),
"auth.passkey.register": t("admin.authPasskeyRegister"),
"auth.passkey.delete": t("admin.authPasskeyDelete"),
"auth.token.autoissue.native": t("admin.authTokenAutoissueNative"),
"auth.token.refresh": t("admin.authTokenRefresh"),
"auth.token.revoke": t("admin.authTokenRevoke"),
};
const entries = data?.entries ?? [];
@@ -116,31 +119,41 @@ export function RecentAuditPreview() {
<ul className="divide-border divide-y">
{entries.map((entry) => {
const isFailed = entry.action === "auth.login.failed";
// F-31 (v1.4.19): each row links into the full
// /admin/login-overview viewer so admins can investigate
// failed logins without re-finding the row by hand.
return (
<li
key={entry.id}
className="flex items-center gap-3 py-2 text-sm"
>
<span className="shrink-0" aria-hidden="true">
{isFailed ? (
<XCircle className="text-destructive h-4 w-4" />
) : (
<CheckCircle2 className="text-dracula-green h-4 w-4" />
<li key={entry.id}>
<Link
href="/admin/login-overview"
className="hover:bg-muted/40 -mx-2 flex items-center gap-3 rounded px-2 py-2 text-sm transition-colors"
>
<span className="shrink-0" aria-hidden="true">
{isFailed ? (
<XCircle className="text-destructive h-4 w-4" />
) : (
<CheckCircle2 className="text-dracula-green h-4 w-4" />
)}
</span>
<span
className={`min-w-0 flex-1 truncate font-medium ${isFailed ? "text-destructive" : ""}`}
>
{entry.user?.username ?? t("common.unknown")}
</span>
<span
className={`hidden truncate sm:inline ${isFailed ? "text-destructive" : "text-muted-foreground"}`}
>
{AUTH_ACTION_LABELS[entry.action] ?? entry.action}
</span>
{entry.ipAddress && (
<span className="text-muted-foreground hidden shrink-0 font-mono text-xs sm:inline">
{entry.ipAddress}
</span>
)}
</span>
<span
className={`min-w-0 flex-1 truncate font-medium ${isFailed ? "text-destructive" : ""}`}
>
{entry.user?.username ?? t("common.unknown")}
</span>
<span
className={`hidden truncate sm:inline ${isFailed ? "text-destructive" : "text-muted-foreground"}`}
>
{AUTH_ACTION_LABELS[entry.action] ?? entry.action}
</span>
<span className="text-muted-foreground shrink-0 text-xs whitespace-nowrap">
{formatDateTime(entry.createdAt)}
</span>
<span className="text-muted-foreground shrink-0 text-xs whitespace-nowrap">
{formatDateTime(entry.createdAt)}
</span>
</Link>
</li>
);
})}