import {useCallback, useEffect, useState} from 'react'; import {Link} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; import {ChevronLeft, ChevronRight, Loader2, Undo2} from 'lucide-react'; import {Button} from '@/components/ui/button'; import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'; import PageContainer from '../components/PageContainer.jsx'; import {toast} from 'sonner'; import {fetchAuditLog, undoMerge} from '../utils/links.js'; import {formatDateTime} from '../utils/format.js'; const PAGE_SIZE = 20; const ACTIONS = ['GAME_MERGE', 'GAME_MERGE_UNDO', 'GAME_LINK', 'GAME_UNLINK', 'GAME_LINK_DISMISS', 'GAME_CORRECT', 'GAME_CORRECTION_RESET']; const ALL = 'ALL'; function GameRef({appid, name, fallback}) { if (appid == null) return null; return {name ?? fallback}; } /** * AUDITOR or higher: the newest rows of Core's audit log (merge, link, unlink, dismiss). Core checks the role. */ export default function AuditLogPage() { const {t, i18n} = useTranslation(); const [action, setAction] = useState(ALL); const [page, setPage] = useState(0); const [result, setResult] = useState({content: [], totalPages: 0}); const [status, setStatus] = useState('loading'); const [undoing, setUndoing] = useState(false); const load = useCallback(async () => { try { const response = await fetchAuditLog({action: action === ALL ? '' : action, page, size: PAGE_SIZE}); if (response.status === 401) return; if (response.status === 403) { setStatus('forbidden'); return; } if (!response.ok) { setStatus('error'); return; } setResult(await response.json()); setStatus('ready'); } catch (err) { console.error('Audit log error:', err); setStatus('error'); } }, [action, page]); useEffect(() => { load(); }, [load]); const onUndo = async (entry) => { // The merged key has no catalog row any more; its name is the first one in the detail. const name = entry.gameName ?? /^"(.*?)" into "/.exec(entry.detail ?? '')?.[1] ?? entry.appid; const other = entry.targetGameName ?? entry.targetAppid; if (!window.confirm(t('AUDIT.UNDO.CONFIRM', {name, other}))) return; setUndoing(true); try { const response = await undoMerge(entry.id); if (response.status === 401) return; if (response.ok) { toast.success(t('AUDIT.UNDO.DONE')); } else { const data = await response.json().catch(() => ({})); toast.error(t(`AUDIT.UNDO.ERROR_CODES.${data.code}`, {defaultValue: t('AUDIT.UNDO.ERROR')})); } await load(); } catch (err) { console.error('Undo merge error:', err); toast.error(t('INFO.ERROR.NETWORK_FAILURE')); } finally { setUndoing(false); } }; const onActionChange = (value) => { setAction(value); setPage(0); }; if (status === 'forbidden') { return (

{t('AUDIT.NO_ACCESS')}

); } return ( {t('AUDIT.TITLE')} {status === 'loading' && } {status === 'error' &&

{t('AUDIT.LOAD_ERROR')}

} {status === 'ready' && result.content.length === 0 && (

{t('AUDIT.EMPTY')}

)} {status === 'ready' && result.content.length > 0 && (
    {result.content.map((entry) => (
  • {formatDateTime(entry.createdAt, i18n.language)} {entry.actorName ?? t('AUDIT.USER_ID', {id: entry.actorUserId})} {t(`AUDIT.ACTION.${entry.action}`)}

    {entry.detail &&

    {entry.detail}

    } {entry.action === 'GAME_MERGE' && entry.undoable && ( )}
  • ))}
)} {status === 'ready' && result.totalPages > 1 && (
{t('AUDIT.PAGE_OF', {current: page + 1, total: result.totalPages})}
)}
); }