import { useMemo } from "react"; import { qk } from "../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Trash2 } from "lucide-react"; import { api } from "../lib/api"; import { notify } from "../lib/notifications"; import { LS } from "../lib/storage"; import { Button } from "./ui/Button"; import { Section } from "./ui/form"; import { useConfirm } from "./confirmContext"; import DataTable from "./ui/DataTable"; import { LoadingState, StateMessage } from "./ui/QueryState"; import { auditColumns } from "./auditColumns"; import { notifyActionError } from "../lib/actionErrors"; import { compareText } from "../lib/collate"; import { foldJobRuns } from "../lib/auditGroups"; // Admin-only audit trail: an append-only who/what/when log of admin actions + scheduler run // summaries (never per-row). Read-only apart from Clear. Client-side sort/filter/paginate via // DataTable; the API returns the most recent window (capped) — the log stays small by design. export default function AuditLog() { const { t, i18n } = useTranslation(); const lang = i18n.language; const qc = useQueryClient(); const confirm = useConfirm(); const q = useQuery({ queryKey: qk.adminAudit(), queryFn: () => api.adminAudit() }); const data = q.data; // Back-to-back successful runs of one job fold into one row (lib/auditGroups). const rows = useMemo(() => foldJobRuns(data?.items ?? []), [data]); const actionOptions = useMemo(() => { // By what the reader sees — the named action, in their alphabet — not by the code. return Array.from(new Set(rows.map((r) => r.action))) .map((a) => ({ value: a, label: t(`auditActions.${a}`, a) })) .sort((a, b) => compareText(a.label, b.label, lang)); }, [rows, t, lang]); const columns = useMemo(() => auditColumns(t, lang, actionOptions), [t, lang, actionOptions]); const clear = useMutation({ mutationFn: () => api.clearAudit(), onSuccess: (r) => { qc.invalidateQueries({ queryKey: qk.adminAudit() }); notify({ level: "success", message: t("audit.cleared", { count: r.cleared }) }); }, onError: (e) => notifyActionError(e, t("audit.clearFailed")), }); const onClear = async () => { if ( await confirm({ title: t("audit.clearTitle"), message: t("audit.clearConfirm"), confirmLabel: t("audit.clear"), danger: true, }) ) clear.mutate(); }; const truncated = !!data && data.total > data.returned; return (
{t("audit.intro")}
{q.isError && !q.data ? ({t("audit.empty")}
) : ( <>{t("audit.truncated", { returned: data!.returned, total: data!.total })}
)} > )}