feat(F-074): completed feature

This commit is contained in:
chattie
2026-08-19 19:25:39 +02:00
parent 1fe9b8babd
commit e699285b1d
12 changed files with 278 additions and 47 deletions

View File

@@ -1,5 +1,5 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import { auditApi, type AuditEntry } from '@/lib/api-client';
const PAGE_SIZE = 50;
@@ -29,6 +29,9 @@ export default function AuditLogPage() {
const [debounced, setDebounced] = useState('');
const [page, setPage] = useState(0);
const [total, setTotal] = useState(0);
const [newCount, setNewCount] = useState(0);
const [pollingActive, setPollingActive] = useState(false);
const itemsRef = useRef<AuditEntry[]>([]);
useEffect(() => {
const t = setTimeout(() => setDebounced(filter), 400);
@@ -49,10 +52,50 @@ export default function AuditLogPage() {
useEffect(() => { load(); }, [load]);
// Keep ref in sync with items state for polling deduplication
useEffect(() => { itemsRef.current = items; }, [items]);
// Real-time polling: fetch new entries every 5s when on page 0 with no filter
useEffect(() => {
if (page !== 0 || debounced) {
setPollingActive(false);
return;
}
setPollingActive(true);
const interval = setInterval(async () => {
try {
const data = await auditApi.list({ limit: PAGE_SIZE, offset: 0 });
const existingIds = new Set(itemsRef.current.map(i => i.id));
const newItems = (data.items ?? []).filter(i => !existingIds.has(i.id));
if (newItems.length > 0) {
setItems(prev => [...newItems, ...prev].slice(0, PAGE_SIZE));
setNewCount(prev => prev + newItems.length);
}
} catch { /* silent */ }
}, 5000);
return () => { clearInterval(interval); setPollingActive(false); };
}, [page, debounced]);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">Log de auditoría</h1>
<h1 className="text-2xl font-bold text-gray-900">Log de auditoría
{pollingActive && (
<span className="ml-3 inline-flex items-center gap-1.5 text-xs text-green-600 font-medium align-middle">
<span className="inline-block w-2 h-2 rounded-full bg-green-500 animate-pulse" />
En vivo
</span>
)}
{newCount > 0 && (
<button
onClick={() => setNewCount(0)}
title="Nuevas entradas — haz clic para marcar como vistas"
className="ml-2 px-2 py-0.5 bg-green-100 text-green-700 text-xs rounded-full hover:bg-green-200 transition-colors"
>
+{newCount} nueva{newCount !== 1 ? 's' : ''}
</button>
)}
</h1>
<p className="text-sm text-gray-500 mt-0.5">
{total > 0 ? `${total} entrada${total !== 1 ? 's' : ''}` : 'Sin entradas'}
</p>