81 lines
2.9 KiB
Markdown
81 lines
2.9 KiB
Markdown
# F-074 — Architect: Audit shows real-time logs from application services
|
|
|
|
## Current state
|
|
|
|
The admin audit page (`/audit`) fetches audit log entries once on mount via `useEffect`. There is **no polling** — new entries only appear after a page refresh.
|
|
|
|
## Goal
|
|
|
|
Add **polling** to the audit page so new log entries appear automatically without refresh. Entries from application services (product edits, category changes, tax rate toggles, etc.) will show up in real-time on the audit page.
|
|
|
|
## Scope IN
|
|
|
|
- `project/apps/admin/src/app/(dashboard)/audit/page.tsx`:
|
|
- Add a `useEffect` with `setInterval` (5 second interval) that fetches the latest entries when `page === 0` (first page)
|
|
- Prepend new entries to the list (deduplicate by `id`)
|
|
- Add a `liveIndicator` state to show "● En vivo" when polling is active on page 0
|
|
- Stop polling when navigating to page > 0 or when filter is active
|
|
- Show count of new entries (e.g., "+3 nuevas" badge) when new entries arrive
|
|
- Clear new entry count when user views them
|
|
|
|
## Scope OUT
|
|
|
|
- Backend — no changes (existing `GET /admin/audit` is sufficient for polling)
|
|
- No SSE/WebSocket (simple polling is sufficient)
|
|
- No changes to the audit logger itself
|
|
|
|
## Design
|
|
|
|
Polling strategy:
|
|
```tsx
|
|
const [newCount, setNewCount] = useState(0);
|
|
const [pollingActive, setPollingActive] = useState(false);
|
|
|
|
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(items.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, items]);
|
|
```
|
|
|
|
UI indicator:
|
|
```tsx
|
|
// In header:
|
|
{pollingActive && (
|
|
<span className="inline-flex items-center gap-1.5 text-xs text-green-600 font-medium">
|
|
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
|
En vivo
|
|
</span>
|
|
)}
|
|
{newCount > 0 && (
|
|
<button onClick={() => setNewCount(0)}
|
|
className="ml-2 px-2 py-0.5 bg-green-100 text-green-700 text-xs rounded-full">
|
|
+{newCount} nueva{newCount !== 1 ? 's' : ''}
|
|
</button>
|
|
)}
|
|
```
|
|
|
|
## Risk
|
|
|
|
- **Low risk**: purely frontend polling, no backend or DB changes
|
|
- No performance concern: 5-second polling is lightweight
|
|
- Polling stops automatically when filter is active or on paginated views
|
|
|
|
## Verification
|
|
|
|
- `npx tsc --noEmit` admin
|
|
- `npx eslint` on changed file
|
|
- `./scripts/verify.sh` green
|
|
- Manual: open audit page, perform an action (e.g. toggle a tax rate), observe new entry appears within 5 seconds
|