mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
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.
149 lines
5.3 KiB
TypeScript
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>
|
|
);
|
|
}
|