Files
siftlode/frontend/src/components/AuditLog.tsx
T
peter 34297b696f refactor(fe): migrate plex/messaging/admin/downloads/notifications to qk (R7 S2b)
Extend lib/queryKeys.ts to the remaining ~34 key roots and migrate every
remaining literal (queryKey: [...] AND the positional useLiveQuery keys) in
36 files. All ~200 React Query keys across the app now route through the
factory; zero raw key literals remain outside queryKeys.ts.

The heterogeneous composite plex keys (plex-library/facets/collections/
playlists carry a filters object, a ratingKeys array, or a union/group
discriminator) take a variadic pass-through — the factory centralizes the
ROOT string; single-id keys stay precisely typed. Nullable keys (thread,
playlist, ytSearch) accept null so a null segment is preserved.

Pure substitution — byte-identical key arrays, so TanStack prefix matching
is unchanged. Gate green: typecheck (app+node+e2e), eslint (0 err),
prettier, 68 vitest. Runtime-verified on a fresh dev server: Feed + Plex
modules load, feed/facets/tags/my-status/plex-library/plex-facets/
plex-collections/plex-playlists all 200, full UI renders.
2026-07-27 22:10:03 +02:00

97 lines
3.6 KiB
TypeScript

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 { Section } from "./ui/form";
import { useConfirm } from "./ConfirmProvider";
import DataTable from "./DataTable";
import { LoadingState, StateMessage } from "./QueryState";
import { auditColumns } from "./auditColumns";
// 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 } = useTranslation();
const qc = useQueryClient();
const confirm = useConfirm();
const q = useQuery({ queryKey: qk.adminAudit(), queryFn: () => api.adminAudit() });
const data = q.data;
const rows = useMemo(() => data?.items ?? [], [data]);
const actionOptions = useMemo(() => {
const present = Array.from(new Set(rows.map((r) => r.action))).sort();
return present.map((a) => ({ value: a, label: t(`auditActions.${a}`, a) }));
}, [rows, t]);
const columns = useMemo(() => auditColumns(t, actionOptions), [t, 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: () => notify({ level: "error", message: 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 (
<div className="p-4 max-w-5xl w-full mx-auto">
<Section card title={t("audit.title")}>
<p className="text-xs text-muted leading-relaxed mb-3">{t("audit.intro")}</p>
{q.isError && !q.data ? (
<StateMessage tone="error" onRetry={() => q.refetch()}>
{t("common.loadError")}
</StateMessage>
) : q.isLoading ? (
<LoadingState />
) : rows.length === 0 ? (
<p className="text-sm text-muted">{t("audit.empty")}</p>
) : (
<>
<DataTable
rows={rows}
columns={columns}
rowKey={(r) => String(r.id)}
persistKey={LS.auditTable}
controlsPosition="top"
controlsLeading={
<button
onClick={onClear}
disabled={clear.isPending}
className="inline-flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-lg border border-border text-muted hover:text-red-400 disabled:opacity-50 transition"
>
<Trash2 className="w-3.5 h-3.5" />
{t("audit.clear")}
</button>
}
emptyText={t("audit.empty")}
/>
{truncated && (
<p className="text-[11px] text-muted mt-2">
{t("audit.truncated", { returned: data!.returned, total: data!.total })}
</p>
)}
</>
)}
</Section>
</div>
);
}