Files
HealthLog/src/components/admin/recent-audit-preview.tsx
T
Marc-André Bombeck a4b204ffe4 refactor(ui): retire the raw dracula utilities for semantic tokens
Migrate every remaining *-dracula-* utility site (127 lint warnings,
193 class occurrences) onto the semantic vocabulary. The swaps are
pixel-identical in both themes — each target token aliases the same
primitive the utility read, including the light-theme AA overrides:

- green/orange/cyan/red → success/warning/info/destructive
- purple → primary (same value in both themes)
- pink → the new --brand-pink token (Coach gradient, Apple-Health
  badge, recording mic): dark #ff79c6 — 6.86:1 card / 5.97:1 bg;
  light #a3134d — 7.32:1 card / 6.83:1 bg (computed)
- yellow sites resolve to their status meaning: warn-level log icon,
  update-available pill and offline-geo hint → warning; the admin
  helpful-rate ramp → success/warning/destructive; the confidence
  meter mirrors the coverage-meter precedent (medium + low share the
  caution tint, the lit count carries the distinction)
- the confidence-badge low tier dropped dead *-dracula-comment/*
  classes (no generated utility) for border-border + bg-muted/50

Close two rule blind spots the sweep surfaced — directional borders
(border-l-dracula-red) and the parenthesised CSS-var shorthand
(bg-(--dracula-green)) — and flip healthlog/no-dracula-utility from
warn to error so the class cannot regrow. var(--dracula-*) references
in chart code stay: they are token references the light theme
overrides, and the chart palette is pinned visually.
2026-07-05 21:28:09 +02:00

149 lines
5.3 KiB
TypeScript

"use client";
/**
* `<RecentAuditPreview>` — compact list of the 10 most recent audit
* entries for the `/admin` overview landing page. The full filterable
* viewer lives at `/admin/login-overview`.
*
* Reuses `/api/admin/audit-log` so the data source matches the
* detail-page section (`<LoginOverviewSection>`).
*/
import Link from "next/link";
import { useQuery } from "@tanstack/react-query";
import {
ArrowRight,
CheckCircle2,
Loader2,
ScrollText,
XCircle,
} from "lucide-react";
import { EmptyState } from "@/components/ui/empty-state";
import { formatDateTime } from "@/lib/format";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { type AdminAuditEntry, useAuthActionLabels } from "./_shared";
import { apiGet } from "@/lib/api/api-fetch";
const PREVIEW_LIMIT = 10;
interface AuditLogResponse {
entries: AdminAuditEntry[];
meta: { total: number; limit: number; offset: number };
}
export function RecentAuditPreview() {
const { t } = useTranslations();
const { data, isLoading, isError } = useQuery({
queryKey: queryKeys.adminAuditOverview(),
queryFn: async () => {
return apiGet<AuditLogResponse>(
`/api/admin/audit-log?limit=${PREVIEW_LIMIT}&filter=auth`,
);
},
// Avoid hitting the DB on every page focus; the overview is the
// type of page admins keep open and re-render frequently.
staleTime: 30_000,
});
const AUTH_ACTION_LABELS = useAuthActionLabels();
const entries = data?.entries ?? [];
return (
<SettingsCard as="section" aria-labelledby="admin-overview-audit-heading">
<SettingsCardHeader
icon={ScrollText}
titleId="admin-overview-audit-heading"
title={t("admin.overview.auditTitle")}
status={
<Link
href="/admin/login-overview"
className="text-primary hover:text-primary/80 inline-flex items-center gap-1 text-sm"
>
{t("admin.overview.auditViewAll")}
<ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
</Link>
}
/>
<div className="mt-4">
{isLoading ? (
<div className="flex items-center gap-2 py-2">
<Loader2
className="text-muted-foreground h-4 w-4 animate-spin motion-reduce:animate-none"
aria-hidden="true"
/>
<span className="text-muted-foreground text-sm">
{t("admin.overview.auditLoading")}
</span>
</div>
) : isError ? (
<div
role="alert"
className="text-destructive bg-destructive/10 border-destructive/30 rounded-md border px-3 py-2 text-sm"
>
{t("admin.overview.auditLoadError")}
</div>
) : entries.length === 0 ? (
// v1.4.15 phase-C5: compact + plain so it nests inside the
// overview card without doubling the dashed border. Reuses
// the existing audit-empty string.
<EmptyState
size="compact"
variant="plain"
icon={<ScrollText className="size-5" />}
title={t("admin.overview.auditEmpty")}
/>
) : (
<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}>
<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-success 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 className="text-muted-foreground shrink-0 text-xs whitespace-nowrap">
{formatDateTime(entry.createdAt)}
</span>
</Link>
</li>
);
})}
</ul>
)}
</div>
</SettingsCard>
);
}