feat(F-074): completed feature
This commit is contained in:
80
work/artifacts/F-074/architect.md
Normal file
80
work/artifacts/F-074/architect.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user