# 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
