import React, { useState, useEffect, useRef } from 'react'; import { Badge, IconButton, Menu, MenuItem, Typography, Box, Divider, Chip, Tooltip, List, ListItem, ListItemText, ListItemIcon, Button } from '@mui/material'; import { Notifications as NotificationsIcon, NotificationsActive as NotificationsActiveIcon } from '@mui/icons-material'; import { Warning as WarningIcon, Error as ErrorIcon, Info as InfoIcon, CheckCircle as CheckCircleIcon } from '@mui/icons-material'; import { billingService } from '../../services/billingService'; import { useAuth } from '@clerk/clerk-react'; import { getTasksNeedingIntervention, TaskNeedingIntervention } from '../../api/schedulerDashboard'; import { apiClient } from '../../api/client'; interface Alert { id: string; type: string; title: string; message: string; severity: 'error' | 'warning' | 'info'; priority: 'high' | 'medium' | 'low'; is_read: boolean; created_at: string; source: 'billing' | 'scheduler' | 'agents' | 'task'; metadata?: Record; groupKey?: string; } interface AlertGroup { id: string; title: string; source: Alert['source']; severity: Alert['severity']; priority: 'high' | 'medium' | 'low'; summary: string; count: number; latestTimestamp: string; alerts: Alert[]; metadata?: Record; actionLabel?: string; actionHref?: string; } interface AlertsBadgeProps { colorMode?: 'light' | 'dark'; } const AlertsBadge: React.FC = ({ colorMode = 'light' }) => { const { userId } = useAuth(); const [anchorEl, setAnchorEl] = useState(null); const [alerts, setAlerts] = useState([]); const [alertGroups, setAlertGroups] = useState([]); const [loading, setLoading] = useState(false); const [unreadCount, setUnreadCount] = useState(0); const open = Boolean(anchorEl); const intervalRef = useRef(null); const isPollingRef = useRef(false); const schedulerDismissedRef = useRef>(new Set()); const getSchedulerStorageKey = (uid: string) => `scheduler_alerts_dismissed_${uid}`; const loadSchedulerDismissed = (uid: string) => { if (!uid) return new Set(); try { const stored = localStorage.getItem(getSchedulerStorageKey(uid)); if (!stored) return new Set(); const parsed = JSON.parse(stored); if (Array.isArray(parsed)) { return new Set(parsed); } return new Set(); } catch { return new Set(); } }; const persistSchedulerDismissed = (uid: string, dismissed: Set) => { if (!uid) return; try { localStorage.setItem(getSchedulerStorageKey(uid), JSON.stringify(Array.from(dismissed))); } catch { // ignore storage errors } }; const dismissSchedulerAlert = (alertId: string) => { if (!userId) return; const updated = new Set(schedulerDismissedRef.current); updated.add(alertId); schedulerDismissedRef.current = updated; persistSchedulerDismissed(userId, updated); }; useEffect(() => { if (!userId) return; schedulerDismissedRef.current = loadSchedulerDismissed(userId); }, [userId]); // Fetch all alerts const rebuildGroups = (alertList: Alert[]) => { const groups = buildAlertGroups(alertList); setAlertGroups(groups); const unreadGroups = groups.filter(group => group.alerts.some(alert => !alert.is_read)).length; setUnreadCount(unreadGroups); }; const fetchAlerts = async () => { if (!userId || isPollingRef.current) return; try { isPollingRef.current = true; setLoading(true); const allAlerts: Alert[] = []; // Phase 1: Fetch billing alerts try { const billingAlerts = await billingService.getUsageAlerts(userId, true); const formattedBillingAlerts: Alert[] = billingAlerts.map((alert: any) => ({ id: `billing-${alert.id}`, type: alert.type, title: alert.title || 'Billing Alert', message: alert.message, severity: alert.severity || 'warning', priority: mapSeverityToPriority(alert.severity || 'warning'), is_read: alert.is_read || false, created_at: alert.created_at, source: 'billing' as const, groupKey: `billing-${alert.type}-${alert.title || 'alert'}` })); allAlerts.push(...formattedBillingAlerts); } catch (error) { console.error('Error fetching billing alerts:', error); } // Phase 2: Fetch scheduler/task alerts try { const taskAlerts = await getTasksNeedingIntervention(userId); const formattedSchedulerAlerts: Alert[] = taskAlerts.map((task: TaskNeedingIntervention) => { const alertId = `scheduler-${task.task_type}-${task.task_id}`; const failureReason = task.failure_pattern?.failure_reason || 'unknown'; const reasonInfo = failureReasonDetails[failureReason] || failureReasonDetails.unknown; const taskLabel = formatTaskDisplayName(task); const message = buildSchedulerAlertMessage(task); const timestamp = task.failure_pattern?.last_failure_time || task.last_failure || new Date().toISOString(); return { id: alertId, type: 'scheduler_task_failure', title: `Task needs attention: ${taskLabel}`, message, severity: reasonInfo.severity, priority: mapSchedulerReasonToPriority(failureReason), is_read: schedulerDismissedRef.current.has(alertId), created_at: timestamp, source: 'scheduler' as const, metadata: { taskId: task.task_id, taskType: task.task_type, failureReason, occurrences: task.failure_pattern?.consecutive_failures ?? 0, lastFailure: timestamp, }, groupKey: `scheduler-${task.task_type}-${task.task_id}` }; }); allAlerts.push(...formattedSchedulerAlerts); } catch (error) { console.error('Error fetching scheduler alerts:', error); } // Phase 3: Fetch agents team alerts try { const resp = await apiClient.get('/api/agents/alerts', { params: { unread_only: true, limit: 50 } }); const agentAlerts = resp?.data?.data?.alerts || []; const formattedAgentAlerts: Alert[] = agentAlerts.map((a: any) => ({ id: `agents-${a.id}`, type: a.type || 'agent_alert', title: a.title || 'Agents Alert', message: a.message || '', severity: (a.severity as any) || 'info', priority: mapSeverityToPriority(a.severity || 'info'), is_read: Boolean(a.read_at), created_at: a.created_at || new Date().toISOString(), source: 'agents' as const, metadata: { ctaPath: a.cta_path, payload: a.payload, }, groupKey: `agents-${a.type || 'agent_alert'}-${a.title || 'alert'}` })); allAlerts.push(...formattedAgentAlerts); } catch (error) { console.error('Error fetching agent alerts:', error); } // Sort alerts by created_at (newest first) allAlerts.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); setAlerts(allAlerts); rebuildGroups(allAlerts); } catch (error) { console.error('Error fetching alerts:', error); } finally { setLoading(false); isPollingRef.current = false; } }; // Poll for alerts useEffect(() => { if (!userId) return; fetchAlerts(); // Poll every 60 seconds intervalRef.current = setInterval(() => { fetchAlerts(); }, 60000); return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [userId]); const handleOpen = (e: React.MouseEvent) => { setAnchorEl(e.currentTarget); // Refresh alerts when menu opens fetchAlerts(); }; const handleClose = () => { setAnchorEl(null); }; const handleMarkAsRead = async (alert: Alert) => { try { if (alert.source === 'billing') { const numericId = Number(alert.id.replace('billing-', '')); if (!Number.isNaN(numericId)) { await billingService.markAlertRead(numericId); } } else if (alert.source === 'scheduler') { dismissSchedulerAlert(alert.id); } else if (alert.source === 'agents') { const numericId = Number(alert.id.replace('agents-', '')); if (!Number.isNaN(numericId)) { await apiClient.post(`/api/agents/alerts/${numericId}/mark-read`); } } // Update local state const updated = alerts.map(a => (a.id === alert.id ? { ...a, is_read: true } : a)); setAlerts(updated); rebuildGroups(updated); } catch (error) { console.error('Error marking alert as read:', error); } }; const handleGroupClick = async (group: AlertGroup) => { for (const alert of group.alerts.filter(a => !a.is_read)) { await handleMarkAsRead(alert); } }; const handleMarkAllAsRead = async () => { try { for (const alert of alerts.filter(a => !a.is_read)) { await handleMarkAsRead(alert); } } catch (error) { console.error('Error marking all alerts as read:', error); } }; const getSeverityIcon = (severity: string) => { switch (severity) { case 'error': return ; case 'warning': return ; case 'info': return ; default: return ; } }; const getSeverityColor = (severity: string) => { switch (severity) { case 'error': return '#f44336'; case 'warning': return '#ff9800'; case 'info': return '#2196f3'; default: return '#757575'; } }; const formatDate = (dateString: string) => { const date = new Date(dateString); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return 'Just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); }; if (!userId) return null; return ( <> 0 ? `${unreadCount} unread alert${unreadCount > 1 ? 's' : ''}` : 'No alerts'}> {unreadCount > 0 ? ( ) : ( )} Alerts {unreadCount > 0 && ( )} {loading && alertGroups.length === 0 ? ( Loading alerts... ) : alertGroups.length === 0 ? ( No alerts ) : ( {alertGroups.map((group, index) => ( a.is_read) ? 'transparent' : 'rgba(255, 152, 0, 0.05)', '&:hover': { bgcolor: 'rgba(0,0,0,0.04)', }, cursor: 'pointer', }} onClick={() => handleGroupClick(group)} > {getSeverityIcon(group.severity)} a.is_read) ? 400 : 700 }}> {group.title} 1 ? 's' : ''}`} size="small" sx={{ height: 18, fontSize: '0.65rem', bgcolor: 'rgba(0,0,0,0.08)', color: colorMode === 'dark' ? 'white' : 'inherit', }} /> } secondary={ <> {group.summary} {group.alerts.slice(0, 2).map((alert, idx) => ( • {alert.message} ))} Last alert: {formatDate(group.latestTimestamp)} {group.actionHref && ( )} } /> {index < alertGroups.length - 1 && } ))} )} {alertGroups.length > 0 && ( <> )} ); }; const failureReasonDetails: Record = { api_limit: { label: 'API limit exceeded', severity: 'error', guidance: 'Usage quota exceeded. Consider upgrading or waiting for quota reset.', }, auth_error: { label: 'Authentication error', severity: 'error', guidance: 'Refresh your platform credentials and retry the task.', }, network_error: { label: 'Network error', severity: 'warning', guidance: 'Network instability detected. Retry once connectivity is restored.', }, config_error: { label: 'Configuration issue', severity: 'warning', guidance: 'Review task configuration and ensure required inputs are set.', }, unknown: { label: 'Unknown failure', severity: 'info', guidance: 'Check task logs for more details.', }, }; const formatTaskDisplayName = (task: TaskNeedingIntervention): string => { if (task.task_type === 'oauth_token_monitoring') { return `OAuth ${task.platform?.toUpperCase() || 'Token'}`; } if (task.task_type === 'website_analysis') { if (task.website_url) { return `Website Analysis (${task.website_url})`; } return 'Website Analysis'; } if (task.task_type.includes('_insights')) { return `${task.platform?.toUpperCase() || 'Platform'} Insights`; } return task.task_type.replace(/_/g, ' '); }; const buildSchedulerAlertMessage = (task: TaskNeedingIntervention): string => { const reasonKey = task.failure_pattern?.failure_reason || 'unknown'; const reasonInfo = failureReasonDetails[reasonKey] || failureReasonDetails.unknown; const consecutive = task.failure_pattern?.consecutive_failures ?? 0; const recent = task.failure_pattern?.recent_failures ?? 0; return `${reasonInfo.label}. ${consecutive} consecutive failures, ${recent} in the last 7 days. ${reasonInfo.guidance}`; }; const getAlertAction = (alert: Alert): { label?: string; href?: string } => { if (alert.source === 'billing') { return { label: 'Open Billing', href: '/billing', }; } if (alert.source === 'scheduler') { const taskId = alert.metadata?.taskId; if (taskId) { return { label: `Review Task #${taskId}`, href: `/scheduler?taskId=${taskId}`, }; } return { label: 'View Scheduler', href: '/scheduler#tasks', }; } if (alert.source === 'agents') { const ctaPath = alert.metadata?.ctaPath; if (typeof ctaPath === 'string' && ctaPath.trim()) { return { label: 'Open', href: ctaPath }; } return { label: 'View Agents', href: '/content-planning' }; } if (alert.source === 'task') { return { label: 'View Tasks', href: '/tasks', }; } return {}; }; const mapSeverityToPriority = (severity: string): 'high' | 'medium' | 'low' => { if (severity === 'error') return 'high'; if (severity === 'warning') return 'medium'; return 'low'; }; const mapSchedulerReasonToPriority = (reason: string): 'high' | 'medium' | 'low' => { switch (reason) { case 'api_limit': case 'auth_error': return 'high'; case 'network_error': return 'medium'; default: return 'low'; } }; const priorityRank: Record<'high' | 'medium' | 'low', number> = { high: 0, medium: 1, low: 2, }; const priorityStyles: Record<'high' | 'medium' | 'low', { bg: string; color: string }> = { high: { bg: 'rgba(244,67,54,0.15)', color: '#f44336' }, medium: { bg: 'rgba(255,152,0,0.2)', color: '#ff9800' }, low: { bg: 'rgba(33,150,243,0.15)', color: '#2196f3' }, }; const buildAlertGroups = (alertList: Alert[]): AlertGroup[] => { const map = new Map(); for (const alert of alertList) { const key = alert.groupKey || `${alert.source}-${alert.type}-${alert.title}`; const existing = map.get(key); const timestamp = alert.created_at; if (existing) { existing.count += 1; existing.alerts.push(alert); if (new Date(timestamp).getTime() > new Date(existing.latestTimestamp).getTime()) { existing.latestTimestamp = timestamp; existing.summary = alert.message; } if (priorityRank[alert.priority] < priorityRank[existing.priority]) { existing.priority = alert.priority; existing.severity = alert.severity; } } else { const action = getAlertAction(alert); map.set(key, { id: key, title: alert.title, source: alert.source, severity: alert.severity, priority: alert.priority, summary: alert.message, count: 1, latestTimestamp: timestamp, alerts: [alert], metadata: alert.metadata, actionLabel: action.label, actionHref: action.href, }); } } return Array.from(map.values()).sort((a, b) => { const priorityCompare = priorityRank[a.priority] - priorityRank[b.priority]; if (priorityCompare !== 0) return priorityCompare; return new Date(b.latestTimestamp).getTime() - new Date(a.latestTimestamp).getTime(); }); }; export default AlertsBadge;