New alert system and panel de control page
This commit is contained in:
318
frontend/src/hooks/useEventNotifications.ts
Normal file
318
frontend/src/hooks/useEventNotifications.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* useEventNotifications Hook
|
||||
*
|
||||
* Subscribe to lightweight informational notifications from the new event architecture.
|
||||
* Subscribes to domain-specific notification channels via SSE.
|
||||
*
|
||||
* Examples:
|
||||
* const { notifications } = useEventNotifications(); // All notifications
|
||||
* const { notifications } = useEventNotifications({ domains: ['production'], maxAge: 3600 });
|
||||
* const { notifications } = useEventNotifications({ eventTypes: ['batch_completed', 'batch_started'] });
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { useSSEEvents } from './useSSE';
|
||||
import type {
|
||||
Notification,
|
||||
EventDomain,
|
||||
UseNotificationsConfig,
|
||||
} from '../types/events';
|
||||
import { isNotification } from '../types/events';
|
||||
|
||||
interface UseEventNotificationsReturn {
|
||||
notifications: Notification[];
|
||||
recentNotifications: Notification[];
|
||||
isLoading: boolean;
|
||||
clearNotifications: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_MAX_AGE = 3600; // 1 hour
|
||||
|
||||
export function useEventNotifications(config: UseNotificationsConfig = {}): UseEventNotificationsReturn {
|
||||
const {
|
||||
domains,
|
||||
eventTypes,
|
||||
maxAge = DEFAULT_MAX_AGE,
|
||||
} = config;
|
||||
|
||||
// Determine which channels to subscribe to
|
||||
const channels = useMemo(() => {
|
||||
if (!domains || domains.length === 0) {
|
||||
// Subscribe to all notification channels
|
||||
return ['*.notifications'];
|
||||
}
|
||||
// Subscribe to specific domain notification channels
|
||||
return domains.map(domain => `${domain}.notifications`);
|
||||
}, [domains]);
|
||||
|
||||
// Subscribe to SSE with channel filter
|
||||
const { events, isConnected } = useSSEEvents({ channels });
|
||||
|
||||
// Filter events to notifications only
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Use refs to track previous values and prevent unnecessary updates
|
||||
const prevEventIdsRef = useRef<string>('');
|
||||
const prevEventTypesRef = useRef<string[]>([]);
|
||||
const prevMaxAgeRef = useRef<number>(maxAge);
|
||||
const prevDomainsRef = useRef<string[]>(domains || []);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if the configuration has actually changed
|
||||
const currentEventTypes = eventTypes || [];
|
||||
const currentDomains = domains || [];
|
||||
|
||||
const configChanged =
|
||||
JSON.stringify(currentEventTypes) !== JSON.stringify(prevEventTypesRef.current) ||
|
||||
prevMaxAgeRef.current !== maxAge ||
|
||||
JSON.stringify(currentDomains) !== JSON.stringify(prevDomainsRef.current);
|
||||
|
||||
// Update refs with current values
|
||||
prevEventTypesRef.current = currentEventTypes;
|
||||
prevMaxAgeRef.current = maxAge;
|
||||
prevDomainsRef.current = currentDomains;
|
||||
|
||||
// Create current event IDs string for comparison
|
||||
const currentEventIds = events.map(e => e.id).join(',');
|
||||
|
||||
// Only process if config changed OR events actually changed
|
||||
if (!configChanged && currentEventIds === prevEventIdsRef.current) {
|
||||
return; // No changes, skip processing
|
||||
}
|
||||
|
||||
// Update the previous event IDs
|
||||
prevEventIdsRef.current = currentEventIds;
|
||||
|
||||
console.log('🔵 [useEventNotifications] Effect triggered', {
|
||||
eventsCount: events.length,
|
||||
eventTypes,
|
||||
maxAge,
|
||||
domains,
|
||||
channels,
|
||||
});
|
||||
|
||||
// Filter and process incoming events
|
||||
const notificationEvents = events.filter(isNotification);
|
||||
console.log('🔵 [useEventNotifications] After isNotification filter:', {
|
||||
notificationEventsCount: notificationEvents.length,
|
||||
eventIds: notificationEvents.map(e => e.id).join(','),
|
||||
});
|
||||
|
||||
// Apply filters
|
||||
let filtered = notificationEvents;
|
||||
|
||||
// Filter by event types
|
||||
if (eventTypes && eventTypes.length > 0) {
|
||||
filtered = filtered.filter(notification =>
|
||||
eventTypes.includes(notification.event_type)
|
||||
);
|
||||
console.log('🔵 [useEventNotifications] After event type filter:', {
|
||||
filteredCount: filtered.length,
|
||||
requestedTypes: eventTypes,
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by age (maxAge in seconds)
|
||||
if (maxAge) {
|
||||
const maxAgeMs = maxAge * 1000;
|
||||
const now = Date.now();
|
||||
filtered = filtered.filter(notification => {
|
||||
const notificationTime = new Date(notification.created_at).getTime();
|
||||
return now - notificationTime <= maxAgeMs;
|
||||
});
|
||||
console.log('🔵 [useEventNotifications] After age filter:', {
|
||||
filteredCount: filtered.length,
|
||||
maxAge,
|
||||
});
|
||||
}
|
||||
|
||||
// Filter expired notifications (TTL)
|
||||
filtered = filtered.filter(notification => {
|
||||
if (notification.expires_at) {
|
||||
const expiryTime = new Date(notification.expires_at).getTime();
|
||||
return Date.now() < expiryTime;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
console.log('🔵 [useEventNotifications] After expiry filter:', {
|
||||
filteredCount: filtered.length,
|
||||
});
|
||||
|
||||
// Sort by timestamp (newest first)
|
||||
filtered.sort((a, b) => {
|
||||
const timeA = new Date(a.created_at).getTime();
|
||||
const timeB = new Date(b.created_at).getTime();
|
||||
return timeB - timeA;
|
||||
});
|
||||
|
||||
console.log('🔵 [useEventNotifications] Setting notifications state', {
|
||||
filteredCount: filtered.length,
|
||||
filteredIds: filtered.map(n => n.id).join(','),
|
||||
firstId: filtered[0]?.id,
|
||||
});
|
||||
|
||||
// Only update state if the IDs have actually changed to prevent infinite loops
|
||||
setNotifications(prev => {
|
||||
const prevIds = prev.map(n => n.id).join(',');
|
||||
const newIds = filtered.map(n => n.id).join(',');
|
||||
|
||||
console.log('🔵 [useEventNotifications] Comparing IDs:', {
|
||||
prevIds,
|
||||
newIds,
|
||||
hasChanged: prevIds !== newIds,
|
||||
});
|
||||
|
||||
if (prevIds === newIds) {
|
||||
console.log('🔵 [useEventNotifications] IDs unchanged, keeping previous array reference');
|
||||
return prev; // Keep same reference if IDs haven't changed
|
||||
}
|
||||
|
||||
console.log('🔵 [useEventNotifications] IDs changed, updating state');
|
||||
return filtered;
|
||||
});
|
||||
setIsLoading(false);
|
||||
}, [events, eventTypes, maxAge, domains, channels]); // Include all dependencies
|
||||
|
||||
// Computed values
|
||||
const recentNotifications = useMemo(() => {
|
||||
// Last 10 notifications
|
||||
return notifications.slice(0, 10);
|
||||
}, [notifications]);
|
||||
|
||||
const clearNotifications = useCallback(() => {
|
||||
setNotifications([]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
notifications,
|
||||
recentNotifications,
|
||||
isLoading: isLoading || !isConnected,
|
||||
clearNotifications,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* useNotificationsByDomain Hook
|
||||
*
|
||||
* Get notifications grouped by domain.
|
||||
*/
|
||||
export function useNotificationsByDomain(config: UseNotificationsConfig = {}) {
|
||||
const { notifications, ...rest } = useEventNotifications(config);
|
||||
|
||||
const notificationsByDomain = useMemo(() => {
|
||||
const grouped: Partial<Record<EventDomain, Notification[]>> = {};
|
||||
|
||||
notifications.forEach(notification => {
|
||||
if (!grouped[notification.event_domain]) {
|
||||
grouped[notification.event_domain] = [];
|
||||
}
|
||||
grouped[notification.event_domain]!.push(notification);
|
||||
});
|
||||
|
||||
return grouped;
|
||||
}, [notifications]);
|
||||
|
||||
return {
|
||||
notifications,
|
||||
notificationsByDomain,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* useProductionNotifications Hook
|
||||
*
|
||||
* Convenience hook for production domain notifications.
|
||||
*/
|
||||
export function useProductionNotifications(eventTypes?: string[]) {
|
||||
return useEventNotifications({
|
||||
domains: ['production'],
|
||||
eventTypes,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* useInventoryNotifications Hook
|
||||
*
|
||||
* Convenience hook for inventory domain notifications.
|
||||
*/
|
||||
export function useInventoryNotifications(eventTypes?: string[]) {
|
||||
return useEventNotifications({
|
||||
domains: ['inventory'],
|
||||
eventTypes,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* useSupplyChainNotifications Hook
|
||||
*
|
||||
* Convenience hook for supply chain domain notifications.
|
||||
*/
|
||||
export function useSupplyChainNotifications(eventTypes?: string[]) {
|
||||
return useEventNotifications({
|
||||
domains: ['supply_chain'],
|
||||
eventTypes,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* useOperationsNotifications Hook
|
||||
*
|
||||
* Convenience hook for operations domain notifications (orchestration).
|
||||
*/
|
||||
export function useOperationsNotifications(eventTypes?: string[]) {
|
||||
return useEventNotifications({
|
||||
domains: ['operations'],
|
||||
eventTypes,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* useBatchNotifications Hook
|
||||
*
|
||||
* Convenience hook for production batch-related notifications.
|
||||
*/
|
||||
export function useBatchNotifications() {
|
||||
return useEventNotifications({
|
||||
domains: ['production'],
|
||||
eventTypes: [
|
||||
'batch_state_changed',
|
||||
'batch_completed',
|
||||
'batch_started',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* useDeliveryNotifications Hook
|
||||
*
|
||||
* Convenience hook for delivery-related notifications.
|
||||
*/
|
||||
export function useDeliveryNotifications() {
|
||||
return useEventNotifications({
|
||||
domains: ['supply_chain'],
|
||||
eventTypes: [
|
||||
'delivery_scheduled',
|
||||
'delivery_arriving_soon',
|
||||
'delivery_received',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* useOrchestrationNotifications Hook
|
||||
*
|
||||
* Convenience hook for orchestration run notifications.
|
||||
*/
|
||||
export function useOrchestrationNotifications() {
|
||||
return useEventNotifications({
|
||||
domains: ['operations'],
|
||||
eventTypes: [
|
||||
'orchestration_run_started',
|
||||
'orchestration_run_completed',
|
||||
'action_created',
|
||||
'action_completed',
|
||||
],
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user