import React, { useState, useEffect, useCallback } from 'react'; import { Activity, RefreshCw } from 'lucide-react'; // ⚠️ CONVENTION: Use relative API path from env var only — avoid hardcoded absolute URL fallback const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const ACTION_LABELS = { cve_create: 'added CVE', cve_edit: 'edited CVE', cve_delete: 'deleted CVE', cve_update_status: 'updated status', cve_nvd_sync: 'ran NVD sync', document_upload: 'uploaded doc', document_delete: 'deleted doc', user_create: 'added user', user_update: 'updated user', user_delete: 'deleted user', jira_ticket_create: 'created ticket', jira_ticket_update: 'updated ticket', jira_ticket_delete: 'deleted ticket', archer_ticket_create: 'created Archer ticket', archer_ticket_update: 'updated Archer ticket', archer_ticket_delete: 'deleted Archer ticket', ivanti_sync: 'synced Ivanti', compliance_upload: 'uploaded compliance', kb_create: 'created KB article', kb_update: 'updated KB article', }; const ACTION_COLORS = { cve_create: '#0EA5E9', cve_edit: '#F59E0B', cve_delete: '#EF4444', cve_nvd_sync: '#10B981', document_upload: '#8B5CF6', document_delete: '#EF4444', user_create: '#0EA5E9', user_update: '#F59E0B', user_delete: '#EF4444', jira_ticket_create: '#F59E0B', ivanti_sync: '#0D9488', compliance_upload: '#10B981', }; function timeAgo(dateStr) { const now = new Date(); const then = new Date(dateStr); const diffMs = now - then; const diffMin = Math.floor(diffMs / 60000); if (diffMin < 1) return 'just now'; if (diffMin < 60) return `${diffMin}m ago`; const diffHr = Math.floor(diffMin / 60); if (diffHr < 24) return `${diffHr}h ago`; const diffDay = Math.floor(diffHr / 24); if (diffDay < 7) return `${diffDay}d ago`; return then.toLocaleDateString(); } function formatActivity(activity) { const label = ACTION_LABELS[activity.action] || activity.action.replace(/_/g, ' '); const entity = activity.entity_id || ''; let detail = ''; if (activity.details) { try { const d = typeof activity.details === 'string' ? JSON.parse(activity.details) : activity.details; if (d.vendor) detail = `(${d.vendor})`; else if (d.cve_id) detail = `(${d.cve_id})`; else if (d.ticket_key) detail = `(${d.ticket_key})`; else if (d.exc_number) detail = `(${d.exc_number})`; else if (d.username) detail = `(${d.username})`; } catch (_e) { /* ignore parse errors */ } } return { label, entity, detail }; } export default function RecentActivityFeed() { const [activities, setActivities] = useState([]); const [loading, setLoading] = useState(true); const fetchActivity = useCallback(async () => { try { const response = await fetch(`${API_BASE}/recent-activity?limit=8`, { credentials: 'include' }); if (!response.ok) return; const data = await response.json(); setActivities(data.activities || []); } catch (_err) { // Silent fail — this is a non-critical widget } finally { setLoading(false); } }, []); useEffect(() => { fetchActivity(); }, [fetchActivity]); // Auto-refresh every 60 seconds useEffect(() => { const interval = setInterval(fetchActivity, 60000); return () => clearInterval(interval); }, [fetchActivity]); return (

Recent Activity

{loading ? (

Loading...

) : activities.length === 0 ? (

No recent activity

) : (
{activities.map((a, idx) => { const { label, entity, detail } = formatActivity(a); const color = ACTION_COLORS[a.action] || '#64748B'; return (
{a.username} {' '}{label} {entity && {entity}} {detail && {detail}}
{timeAgo(a.created_at)}
); })}
)}
); }