diff --git a/components/realtime/ConnectionStatus.tsx b/components/realtime/ConnectionStatus.tsx
new file mode 100644
index 0000000..110c487
--- /dev/null
+++ b/components/realtime/ConnectionStatus.tsx
@@ -0,0 +1,167 @@
+/**
+ * Connection Status Indicator Component
+ *
+ * Shows the current WebSocket connection status with visual feedback.
+ */
+
+import React from 'react';
+import classNames from 'classnames';
+import { useConnectionStatus } from '../../lib/realtime/useSocket';
+import type { ConnectionStatus as ConnectionStatusType } from '../../lib/realtime/types';
+
+interface ConnectionStatusProps {
+ showLabel?: boolean;
+ showLatency?: boolean;
+ size?: 'sm' | 'md' | 'lg';
+ className?: string;
+}
+
+/**
+ * Get status color classes
+ */
+function getStatusColor(status: ConnectionStatusType): string {
+ switch (status) {
+ case 'connected':
+ return 'bg-green-500';
+ case 'connecting':
+ case 'reconnecting':
+ return 'bg-yellow-500 animate-pulse';
+ case 'disconnected':
+ return 'bg-gray-400';
+ case 'error':
+ return 'bg-red-500';
+ default:
+ return 'bg-gray-400';
+ }
+}
+
+/**
+ * Get status label
+ */
+function getStatusLabel(status: ConnectionStatusType): string {
+ switch (status) {
+ case 'connected':
+ return 'Connected';
+ case 'connecting':
+ return 'Connecting...';
+ case 'reconnecting':
+ return 'Reconnecting...';
+ case 'disconnected':
+ return 'Disconnected';
+ case 'error':
+ return 'Connection Error';
+ default:
+ return 'Unknown';
+ }
+}
+
+/**
+ * Get size classes
+ */
+function getSizeClasses(size: 'sm' | 'md' | 'lg'): { dot: string; text: string } {
+ switch (size) {
+ case 'sm':
+ return { dot: 'w-2 h-2', text: 'text-xs' };
+ case 'md':
+ return { dot: 'w-3 h-3', text: 'text-sm' };
+ case 'lg':
+ return { dot: 'w-4 h-4', text: 'text-base' };
+ default:
+ return { dot: 'w-3 h-3', text: 'text-sm' };
+ }
+}
+
+/**
+ * Connection Status component
+ */
+export function ConnectionStatus({
+ showLabel = true,
+ showLatency = false,
+ size = 'md',
+ className,
+}: ConnectionStatusProps) {
+ const { status, latency } = useConnectionStatus();
+ const sizeClasses = getSizeClasses(size);
+
+ return (
+
+ {/* Status dot */}
+
+
+ {/* Label */}
+ {showLabel && (
+
+ {getStatusLabel(status)}
+
+ )}
+
+ {/* Latency */}
+ {showLatency && status === 'connected' && latency !== undefined && (
+
+ ({latency}ms)
+
+ )}
+
+ );
+}
+
+/**
+ * Compact connection indicator (dot only)
+ */
+export function ConnectionDot({ className }: { className?: string }) {
+ const { status } = useConnectionStatus();
+
+ return (
+
+ );
+}
+
+/**
+ * Connection banner for showing reconnection status
+ */
+export function ConnectionBanner() {
+ const { status } = useConnectionStatus();
+
+ if (status === 'connected') {
+ return null;
+ }
+
+ const bannerClasses = classNames(
+ 'fixed top-0 left-0 right-0 py-2 px-4 text-center text-sm font-medium z-50',
+ {
+ 'bg-yellow-100 text-yellow-800': status === 'connecting' || status === 'reconnecting',
+ 'bg-red-100 text-red-800': status === 'error',
+ 'bg-gray-100 text-gray-800': status === 'disconnected',
+ }
+ );
+
+ return (
+
+ {status === 'connecting' && 'Connecting to real-time updates...'}
+ {status === 'reconnecting' && 'Connection lost. Reconnecting...'}
+ {status === 'error' && 'Connection error. Please check your network.'}
+ {status === 'disconnected' && 'Disconnected from real-time updates.'}
+
+ );
+}
+
+export default ConnectionStatus;
diff --git a/components/realtime/LiveChart.tsx b/components/realtime/LiveChart.tsx
new file mode 100644
index 0000000..c61825b
--- /dev/null
+++ b/components/realtime/LiveChart.tsx
@@ -0,0 +1,256 @@
+/**
+ * Live Chart Component
+ *
+ * Displays real-time data as a simple line chart.
+ */
+
+import React, { useMemo } from 'react';
+import classNames from 'classnames';
+import { useSocket } from '../../lib/realtime/useSocket';
+import type { TransparencyEventType } from '../../lib/realtime/types';
+
+interface LiveChartProps {
+ eventTypes?: TransparencyEventType[];
+ dataKey?: string;
+ title?: string;
+ color?: string;
+ height?: number;
+ maxDataPoints?: number;
+ showGrid?: boolean;
+ className?: string;
+}
+
+/**
+ * Simple SVG line chart for real-time data
+ */
+export function LiveChart({
+ eventTypes = ['system.metric'],
+ dataKey = 'value',
+ title = 'Live Data',
+ color = '#3B82F6',
+ height = 120,
+ maxDataPoints = 30,
+ showGrid = true,
+ className,
+}: LiveChartProps) {
+ const { events } = useSocket({
+ eventTypes,
+ maxEvents: maxDataPoints,
+ });
+
+ // Extract data points
+ const dataPoints = useMemo(() => {
+ return events
+ .filter((e) => e.data && typeof e.data[dataKey] === 'number')
+ .map((e) => ({
+ value: e.data[dataKey] as number,
+ timestamp: new Date(e.timestamp).getTime(),
+ }))
+ .reverse()
+ .slice(-maxDataPoints);
+ }, [events, dataKey, maxDataPoints]);
+
+ // Calculate chart dimensions
+ const chartWidth = 400;
+ const chartHeight = height - 40;
+ const padding = { top: 10, right: 10, bottom: 20, left: 40 };
+ const innerWidth = chartWidth - padding.left - padding.right;
+ const innerHeight = chartHeight - padding.top - padding.bottom;
+
+ // Calculate scales
+ const { minValue, maxValue, points, pathD } = useMemo(() => {
+ if (dataPoints.length === 0) {
+ return { minValue: 0, maxValue: 100, points: [], pathD: '' };
+ }
+
+ const values = dataPoints.map((d) => d.value);
+ const min = Math.min(...values);
+ const max = Math.max(...values);
+ const range = max - min || 1;
+
+ const pts = dataPoints.map((d, i) => ({
+ x: padding.left + (i / Math.max(1, dataPoints.length - 1)) * innerWidth,
+ y: padding.top + innerHeight - ((d.value - min) / range) * innerHeight,
+ }));
+
+ const d = pts.length > 0
+ ? `M ${pts.map((p) => `${p.x},${p.y}`).join(' L ')}`
+ : '';
+
+ return { minValue: min, maxValue: max, points: pts, pathD: d };
+ }, [dataPoints, innerWidth, innerHeight, padding]);
+
+ // Latest value
+ const latestValue = dataPoints.length > 0 ? dataPoints[dataPoints.length - 1].value : null;
+
+ return (
+
+ {/* Header */}
+
+
{title}
+ {latestValue !== null && (
+
+ {latestValue.toFixed(1)}
+
+ )}
+
+
+ {/* Chart */}
+
+
+ );
+}
+
+/**
+ * Event count chart - shows event frequency over time
+ */
+export function EventCountChart({
+ className,
+}: {
+ className?: string;
+}) {
+ const { events } = useSocket({ maxEvents: 100 });
+
+ // Group events by minute
+ const countsByMinute = useMemo(() => {
+ const counts: Record = {};
+ const now = Date.now();
+
+ // Initialize last 10 minutes
+ for (let i = 0; i < 10; i++) {
+ const minute = Math.floor((now - i * 60000) / 60000);
+ counts[minute] = 0;
+ }
+
+ // Count events
+ events.forEach((e) => {
+ const minute = Math.floor(new Date(e.timestamp).getTime() / 60000);
+ if (counts[minute] !== undefined) {
+ counts[minute]++;
+ }
+ });
+
+ return Object.entries(counts)
+ .sort(([a], [b]) => Number(a) - Number(b))
+ .map(([, count]) => count);
+ }, [events]);
+
+ const maxCount = Math.max(...countsByMinute, 1);
+
+ return (
+
+
Events per Minute
+
+
+ {countsByMinute.map((count, i) => (
+
+ ))}
+
+
+
+ 10m ago
+ Now
+
+
+ );
+}
+
+export default LiveChart;
diff --git a/components/realtime/LiveFeed.tsx b/components/realtime/LiveFeed.tsx
new file mode 100644
index 0000000..0b0297b
--- /dev/null
+++ b/components/realtime/LiveFeed.tsx
@@ -0,0 +1,255 @@
+/**
+ * Live Feed Component
+ *
+ * Displays a real-time feed of events from the LocalGreenChain system.
+ */
+
+import React, { useMemo } from 'react';
+import classNames from 'classnames';
+import { useLiveFeed } from '../../lib/realtime/useSocket';
+import type { LiveFeedItem, RoomType, TransparencyEventType } from '../../lib/realtime/types';
+import { EventCategory, getEventCategory } from '../../lib/realtime/events';
+import { ConnectionStatus } from './ConnectionStatus';
+
+interface LiveFeedProps {
+ rooms?: RoomType[];
+ eventTypes?: TransparencyEventType[];
+ maxItems?: number;
+ showConnectionStatus?: boolean;
+ showTimestamps?: boolean;
+ showClearButton?: boolean;
+ filterCategory?: EventCategory;
+ className?: string;
+ emptyMessage?: string;
+}
+
+/**
+ * Format timestamp for display
+ */
+function formatTimestamp(timestamp: number): string {
+ const date = new Date(timestamp);
+ const now = new Date();
+ const diffMs = now.getTime() - timestamp;
+ const diffSec = Math.floor(diffMs / 1000);
+ const diffMin = Math.floor(diffSec / 60);
+ const diffHour = Math.floor(diffMin / 60);
+
+ if (diffSec < 60) {
+ return 'Just now';
+ } else if (diffMin < 60) {
+ return `${diffMin}m ago`;
+ } else if (diffHour < 24) {
+ return `${diffHour}h ago`;
+ } else {
+ return date.toLocaleDateString();
+ }
+}
+
+/**
+ * Get color classes for event type
+ */
+function getColorClasses(color: string): { bg: string; border: string; text: string } {
+ switch (color) {
+ case 'green':
+ return {
+ bg: 'bg-green-50',
+ border: 'border-green-200',
+ text: 'text-green-800',
+ };
+ case 'blue':
+ return {
+ bg: 'bg-blue-50',
+ border: 'border-blue-200',
+ text: 'text-blue-800',
+ };
+ case 'yellow':
+ return {
+ bg: 'bg-yellow-50',
+ border: 'border-yellow-200',
+ text: 'text-yellow-800',
+ };
+ case 'red':
+ return {
+ bg: 'bg-red-50',
+ border: 'border-red-200',
+ text: 'text-red-800',
+ };
+ case 'purple':
+ return {
+ bg: 'bg-purple-50',
+ border: 'border-purple-200',
+ text: 'text-purple-800',
+ };
+ case 'gray':
+ default:
+ return {
+ bg: 'bg-gray-50',
+ border: 'border-gray-200',
+ text: 'text-gray-800',
+ };
+ }
+}
+
+/**
+ * Single feed item component
+ */
+function FeedItem({
+ item,
+ showTimestamp,
+}: {
+ item: LiveFeedItem;
+ showTimestamp: boolean;
+}) {
+ const colors = getColorClasses(item.formatted.color);
+
+ return (
+
+
+ {/* Icon */}
+
+ {item.formatted.icon}
+
+
+ {/* Content */}
+
+
+
+ {item.formatted.title}
+
+ {showTimestamp && (
+
+ {formatTimestamp(item.timestamp)}
+
+ )}
+
+
+ {item.formatted.description}
+
+
+
+
+ );
+}
+
+/**
+ * Live Feed component
+ */
+export function LiveFeed({
+ rooms,
+ eventTypes,
+ maxItems = 20,
+ showConnectionStatus = true,
+ showTimestamps = true,
+ showClearButton = true,
+ filterCategory,
+ className,
+ emptyMessage = 'No events yet. Real-time updates will appear here.',
+}: LiveFeedProps) {
+ const { items, isConnected, status, clearFeed } = useLiveFeed({
+ rooms,
+ eventTypes,
+ maxEvents: maxItems,
+ });
+
+ // Filter items by category if specified
+ const filteredItems = useMemo(() => {
+ if (!filterCategory) return items;
+
+ return items.filter((item) => {
+ const category = getEventCategory(item.event.type);
+ return category === filterCategory;
+ });
+ }, [items, filterCategory]);
+
+ return (
+
+ {/* Header */}
+
+
+
Live Feed
+ {showConnectionStatus && }
+
+
+
+ {filteredItems.length > 0 && (
+
+ {filteredItems.length} event{filteredItems.length !== 1 ? 's' : ''}
+
+ )}
+ {showClearButton && filteredItems.length > 0 && (
+
+ )}
+
+
+
+ {/* Feed content */}
+
+ {filteredItems.length === 0 ? (
+
+
📡
+
{emptyMessage}
+ {!isConnected && (
+
+ Status: {status}
+
+ )}
+
+ ) : (
+ filteredItems.map((item) => (
+
+ ))
+ )}
+
+
+ );
+}
+
+/**
+ * Compact live feed for sidebars
+ */
+export function CompactLiveFeed({
+ maxItems = 5,
+ className,
+}: {
+ maxItems?: number;
+ className?: string;
+}) {
+ const { items } = useLiveFeed({ maxEvents: maxItems });
+
+ if (items.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {items.slice(0, maxItems).map((item) => (
+
+ {item.formatted.icon}
+
+ {item.formatted.description}
+
+
+ ))}
+
+ );
+}
+
+export default LiveFeed;
diff --git a/components/realtime/NotificationToast.tsx b/components/realtime/NotificationToast.tsx
new file mode 100644
index 0000000..9f99959
--- /dev/null
+++ b/components/realtime/NotificationToast.tsx
@@ -0,0 +1,325 @@
+/**
+ * Notification Toast Component
+ *
+ * Displays real-time notifications as toast messages.
+ */
+
+import React, { useEffect, useState, useCallback } from 'react';
+import classNames from 'classnames';
+import { useSocketContext } from '../../lib/realtime/SocketContext';
+import type { RealtimeNotification } from '../../lib/realtime/types';
+
+interface NotificationToastProps {
+ position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
+ maxVisible?: number;
+ autoHideDuration?: number;
+ className?: string;
+}
+
+/**
+ * Get position classes
+ */
+function getPositionClasses(position: NotificationToastProps['position']): string {
+ switch (position) {
+ case 'top-left':
+ return 'top-4 left-4';
+ case 'bottom-right':
+ return 'bottom-4 right-4';
+ case 'bottom-left':
+ return 'bottom-4 left-4';
+ case 'top-right':
+ default:
+ return 'top-4 right-4';
+ }
+}
+
+/**
+ * Get notification type styles
+ */
+function getTypeStyles(type: RealtimeNotification['type']): {
+ bg: string;
+ border: string;
+ icon: string;
+ iconColor: string;
+} {
+ switch (type) {
+ case 'success':
+ return {
+ bg: 'bg-green-50',
+ border: 'border-green-200',
+ icon: '✓',
+ iconColor: 'text-green-600',
+ };
+ case 'warning':
+ return {
+ bg: 'bg-yellow-50',
+ border: 'border-yellow-200',
+ icon: '⚠',
+ iconColor: 'text-yellow-600',
+ };
+ case 'error':
+ return {
+ bg: 'bg-red-50',
+ border: 'border-red-200',
+ icon: '✕',
+ iconColor: 'text-red-600',
+ };
+ case 'info':
+ default:
+ return {
+ bg: 'bg-blue-50',
+ border: 'border-blue-200',
+ icon: 'ℹ',
+ iconColor: 'text-blue-600',
+ };
+ }
+}
+
+/**
+ * Single toast notification
+ */
+function Toast({
+ notification,
+ onDismiss,
+ autoHideDuration,
+}: {
+ notification: RealtimeNotification;
+ onDismiss: (id: string) => void;
+ autoHideDuration: number;
+}) {
+ const [isVisible, setIsVisible] = useState(false);
+ const [isLeaving, setIsLeaving] = useState(false);
+ const styles = getTypeStyles(notification.type);
+
+ // Animate in
+ useEffect(() => {
+ const timer = setTimeout(() => setIsVisible(true), 10);
+ return () => clearTimeout(timer);
+ }, []);
+
+ // Auto hide
+ useEffect(() => {
+ if (autoHideDuration <= 0) return;
+
+ const timer = setTimeout(() => {
+ handleDismiss();
+ }, autoHideDuration);
+
+ return () => clearTimeout(timer);
+ }, [autoHideDuration]);
+
+ const handleDismiss = useCallback(() => {
+ setIsLeaving(true);
+ setTimeout(() => {
+ onDismiss(notification.id);
+ }, 300);
+ }, [notification.id, onDismiss]);
+
+ return (
+
+
+ {/* Icon */}
+
+ {styles.icon}
+
+
+ {/* Content */}
+
+
+ {notification.title}
+
+
+ {notification.message}
+
+
+
+ {/* Close button */}
+
+
+
+ );
+}
+
+/**
+ * Notification Toast container
+ */
+export function NotificationToast({
+ position = 'top-right',
+ maxVisible = 5,
+ autoHideDuration = 5000,
+ className,
+}: NotificationToastProps) {
+ const { notifications, dismissNotification } = useSocketContext();
+
+ // Only show non-read, non-dismissed notifications
+ const visibleNotifications = notifications
+ .filter((n) => !n.read && !n.dismissed)
+ .slice(0, maxVisible);
+
+ if (visibleNotifications.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {visibleNotifications.map((notification) => (
+
+ ))}
+
+ );
+}
+
+/**
+ * Notification bell with badge
+ */
+export function NotificationBell({
+ onClick,
+ className,
+}: {
+ onClick?: () => void;
+ className?: string;
+}) {
+ const { unreadCount } = useSocketContext();
+
+ return (
+
+ );
+}
+
+/**
+ * Notification list dropdown
+ */
+export function NotificationList({
+ className,
+ onClose,
+}: {
+ className?: string;
+ onClose?: () => void;
+}) {
+ const { notifications, markNotificationRead, markAllRead } = useSocketContext();
+
+ return (
+
+ {/* Header */}
+
+
Notifications
+ {notifications.length > 0 && (
+
+ )}
+
+
+ {/* List */}
+
+ {notifications.length === 0 ? (
+
+ ) : (
+ notifications.map((notification) => (
+
markNotificationRead(notification.id)}
+ >
+
+
+ {getTypeStyles(notification.type).icon}
+
+
+
+ {notification.title}
+
+
+ {notification.message}
+
+
+ {new Date(notification.timestamp).toLocaleTimeString()}
+
+
+
+
+ ))
+ )}
+
+
+ );
+}
+
+export default NotificationToast;
diff --git a/components/realtime/index.ts b/components/realtime/index.ts
new file mode 100644
index 0000000..b08db49
--- /dev/null
+++ b/components/realtime/index.ts
@@ -0,0 +1,27 @@
+/**
+ * Real-Time Components for LocalGreenChain
+ *
+ * Export all real-time UI components.
+ */
+
+export {
+ ConnectionStatus,
+ ConnectionDot,
+ ConnectionBanner,
+} from './ConnectionStatus';
+
+export {
+ LiveFeed,
+ CompactLiveFeed,
+} from './LiveFeed';
+
+export {
+ NotificationToast,
+ NotificationBell,
+ NotificationList,
+} from './NotificationToast';
+
+export {
+ LiveChart,
+ EventCountChart,
+} from './LiveChart';
diff --git a/lib/realtime/SocketContext.tsx b/lib/realtime/SocketContext.tsx
new file mode 100644
index 0000000..e74ed2a
--- /dev/null
+++ b/lib/realtime/SocketContext.tsx
@@ -0,0 +1,235 @@
+/**
+ * Socket.io Context Provider for LocalGreenChain
+ *
+ * Provides socket connection state to the entire application.
+ */
+
+import React, { createContext, useContext, useEffect, useState, useCallback, useRef, ReactNode } from 'react';
+import { getSocketClient, RealtimeSocketClient } from './socketClient';
+import type {
+ ConnectionStatus,
+ TransparencyEvent,
+ RoomType,
+ TransparencyEventType,
+ ConnectionMetrics,
+ RealtimeNotification,
+} from './types';
+import { toFeedItem } from './events';
+
+/**
+ * Socket context value type
+ */
+interface SocketContextValue {
+ // Connection state
+ status: ConnectionStatus;
+ isConnected: boolean;
+ metrics: ConnectionMetrics;
+
+ // Events
+ events: TransparencyEvent[];
+ latestEvent: TransparencyEvent | null;
+
+ // Notifications
+ notifications: RealtimeNotification[];
+ unreadCount: number;
+
+ // Actions
+ connect: () => void;
+ disconnect: () => void;
+ joinRoom: (room: RoomType) => Promise;
+ leaveRoom: (room: RoomType) => Promise;
+ subscribeToTypes: (types: TransparencyEventType[]) => Promise;
+ clearEvents: () => void;
+ markNotificationRead: (id: string) => void;
+ dismissNotification: (id: string) => void;
+ markAllRead: () => void;
+}
+
+const SocketContext = createContext(null);
+
+/**
+ * Provider props
+ */
+interface SocketProviderProps {
+ children: ReactNode;
+ userId?: string;
+ autoConnect?: boolean;
+ maxEvents?: number;
+ maxNotifications?: number;
+}
+
+/**
+ * Convert event to notification
+ */
+function eventToNotification(event: TransparencyEvent): RealtimeNotification {
+ const feedItem = toFeedItem(event);
+
+ let notificationType: RealtimeNotification['type'] = 'info';
+ if (event.priority === 'CRITICAL') notificationType = 'error';
+ else if (event.priority === 'HIGH') notificationType = 'warning';
+ else if (event.type.includes('error')) notificationType = 'error';
+ else if (event.type.includes('completed') || event.type.includes('verified')) notificationType = 'success';
+
+ return {
+ id: event.id,
+ type: notificationType,
+ title: feedItem.formatted.title,
+ message: feedItem.formatted.description,
+ timestamp: feedItem.timestamp,
+ eventType: event.type,
+ data: event.data,
+ read: false,
+ dismissed: false,
+ };
+}
+
+/**
+ * Socket Provider component
+ */
+export function SocketProvider({
+ children,
+ userId,
+ autoConnect = true,
+ maxEvents = 100,
+ maxNotifications = 50,
+}: SocketProviderProps) {
+ const [status, setStatus] = useState('disconnected');
+ const [events, setEvents] = useState([]);
+ const [latestEvent, setLatestEvent] = useState(null);
+ const [notifications, setNotifications] = useState([]);
+ const [metrics, setMetrics] = useState({
+ status: 'disconnected',
+ eventsReceived: 0,
+ reconnectAttempts: 0,
+ rooms: [],
+ });
+
+ const clientRef = useRef(null);
+
+ // Initialize client
+ useEffect(() => {
+ if (typeof window === 'undefined') return;
+
+ const client = getSocketClient({ auth: { userId } });
+ clientRef.current = client;
+
+ // Set up listeners
+ const unsubStatus = client.onStatusChange((newStatus) => {
+ setStatus(newStatus);
+ setMetrics(client.getMetrics());
+ });
+
+ const unsubEvent = client.onEvent((event) => {
+ setLatestEvent(event);
+ setEvents((prev) => [event, ...prev].slice(0, maxEvents));
+ setMetrics(client.getMetrics());
+
+ // Create notification for important events
+ if (event.priority === 'HIGH' || event.priority === 'CRITICAL') {
+ const notification = eventToNotification(event);
+ setNotifications((prev) => [notification, ...prev].slice(0, maxNotifications));
+ }
+ });
+
+ // Auto connect
+ if (autoConnect) {
+ client.connect();
+ }
+
+ // Initial metrics
+ setMetrics(client.getMetrics());
+
+ return () => {
+ unsubStatus();
+ unsubEvent();
+ };
+ }, [autoConnect, userId, maxEvents, maxNotifications]);
+
+ const connect = useCallback(() => {
+ clientRef.current?.connect();
+ }, []);
+
+ const disconnect = useCallback(() => {
+ clientRef.current?.disconnect();
+ }, []);
+
+ const joinRoom = useCallback(async (room: RoomType) => {
+ return clientRef.current?.joinRoom(room) ?? false;
+ }, []);
+
+ const leaveRoom = useCallback(async (room: RoomType) => {
+ return clientRef.current?.leaveRoom(room) ?? false;
+ }, []);
+
+ const subscribeToTypes = useCallback(async (types: TransparencyEventType[]) => {
+ return clientRef.current?.subscribeToTypes(types) ?? false;
+ }, []);
+
+ const clearEvents = useCallback(() => {
+ setEvents([]);
+ setLatestEvent(null);
+ }, []);
+
+ const markNotificationRead = useCallback((id: string) => {
+ setNotifications((prev) =>
+ prev.map((n) => (n.id === id ? { ...n, read: true } : n))
+ );
+ }, []);
+
+ const dismissNotification = useCallback((id: string) => {
+ setNotifications((prev) =>
+ prev.map((n) => (n.id === id ? { ...n, dismissed: true } : n))
+ );
+ }, []);
+
+ const markAllRead = useCallback(() => {
+ setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
+ }, []);
+
+ const unreadCount = notifications.filter((n) => !n.read && !n.dismissed).length;
+
+ const value: SocketContextValue = {
+ status,
+ isConnected: status === 'connected',
+ metrics,
+ events,
+ latestEvent,
+ notifications: notifications.filter((n) => !n.dismissed),
+ unreadCount,
+ connect,
+ disconnect,
+ joinRoom,
+ leaveRoom,
+ subscribeToTypes,
+ clearEvents,
+ markNotificationRead,
+ dismissNotification,
+ markAllRead,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use socket context
+ */
+export function useSocketContext(): SocketContextValue {
+ const context = useContext(SocketContext);
+ if (!context) {
+ throw new Error('useSocketContext must be used within a SocketProvider');
+ }
+ return context;
+}
+
+/**
+ * Hook to optionally use socket context (returns null if not in provider)
+ */
+export function useOptionalSocketContext(): SocketContextValue | null {
+ return useContext(SocketContext);
+}
+
+export default SocketContext;
diff --git a/lib/realtime/events.ts b/lib/realtime/events.ts
new file mode 100644
index 0000000..f546df1
--- /dev/null
+++ b/lib/realtime/events.ts
@@ -0,0 +1,273 @@
+/**
+ * Real-Time Event Definitions for LocalGreenChain
+ *
+ * Defines all real-time event types and utilities for formatting events.
+ */
+
+import type { TransparencyEventType, TransparencyEvent, LiveFeedItem } from './types';
+
+/**
+ * Event categories for grouping and filtering
+ */
+export enum EventCategory {
+ PLANT = 'plant',
+ TRANSPORT = 'transport',
+ DEMAND = 'demand',
+ FARM = 'farm',
+ AGENT = 'agent',
+ BLOCKCHAIN = 'blockchain',
+ SYSTEM = 'system',
+ AUDIT = 'audit',
+}
+
+/**
+ * Real-time event types enum for client use
+ */
+export enum RealtimeEvent {
+ // Plant events
+ PLANT_REGISTERED = 'plant.registered',
+ PLANT_CLONED = 'plant.cloned',
+ PLANT_TRANSFERRED = 'plant.transferred',
+ PLANT_UPDATED = 'plant.updated',
+
+ // Transport events
+ TRANSPORT_STARTED = 'transport.started',
+ TRANSPORT_COMPLETED = 'transport.completed',
+ TRANSPORT_VERIFIED = 'transport.verified',
+
+ // Demand events
+ DEMAND_CREATED = 'demand.created',
+ DEMAND_MATCHED = 'demand.matched',
+ SUPPLY_COMMITTED = 'supply.committed',
+
+ // Farm events
+ FARM_REGISTERED = 'farm.registered',
+ FARM_UPDATED = 'farm.updated',
+ BATCH_STARTED = 'batch.started',
+ BATCH_HARVESTED = 'batch.harvested',
+
+ // Agent events
+ AGENT_ALERT = 'agent.alert',
+ AGENT_TASK_COMPLETED = 'agent.task_completed',
+ AGENT_ERROR = 'agent.error',
+
+ // Blockchain events
+ BLOCKCHAIN_BLOCK_ADDED = 'blockchain.block_added',
+ BLOCKCHAIN_VERIFIED = 'blockchain.verified',
+ BLOCKCHAIN_ERROR = 'blockchain.error',
+
+ // System events
+ SYSTEM_HEALTH = 'system.health',
+ SYSTEM_ALERT = 'system.alert',
+ SYSTEM_METRIC = 'system.metric',
+
+ // Audit events
+ AUDIT_LOGGED = 'audit.logged',
+ AUDIT_ANOMALY = 'audit.anomaly',
+}
+
+/**
+ * Map event types to their categories
+ */
+export const EVENT_CATEGORIES: Record = {
+ 'plant.registered': EventCategory.PLANT,
+ 'plant.cloned': EventCategory.PLANT,
+ 'plant.transferred': EventCategory.PLANT,
+ 'plant.updated': EventCategory.PLANT,
+ 'transport.started': EventCategory.TRANSPORT,
+ 'transport.completed': EventCategory.TRANSPORT,
+ 'transport.verified': EventCategory.TRANSPORT,
+ 'demand.created': EventCategory.DEMAND,
+ 'demand.matched': EventCategory.DEMAND,
+ 'supply.committed': EventCategory.DEMAND,
+ 'farm.registered': EventCategory.FARM,
+ 'farm.updated': EventCategory.FARM,
+ 'batch.started': EventCategory.FARM,
+ 'batch.harvested': EventCategory.FARM,
+ 'agent.alert': EventCategory.AGENT,
+ 'agent.task_completed': EventCategory.AGENT,
+ 'agent.error': EventCategory.AGENT,
+ 'blockchain.block_added': EventCategory.BLOCKCHAIN,
+ 'blockchain.verified': EventCategory.BLOCKCHAIN,
+ 'blockchain.error': EventCategory.BLOCKCHAIN,
+ 'system.health': EventCategory.SYSTEM,
+ 'system.alert': EventCategory.SYSTEM,
+ 'system.metric': EventCategory.SYSTEM,
+ 'audit.logged': EventCategory.AUDIT,
+ 'audit.anomaly': EventCategory.AUDIT,
+};
+
+/**
+ * Event display configuration
+ */
+interface EventDisplay {
+ title: string;
+ icon: string;
+ color: string;
+}
+
+/**
+ * Map event types to display properties
+ */
+export const EVENT_DISPLAY: Record = {
+ 'plant.registered': { title: 'Plant Registered', icon: '🌱', color: 'green' },
+ 'plant.cloned': { title: 'Plant Cloned', icon: '🧬', color: 'green' },
+ 'plant.transferred': { title: 'Plant Transferred', icon: '🔄', color: 'blue' },
+ 'plant.updated': { title: 'Plant Updated', icon: '📝', color: 'gray' },
+ 'transport.started': { title: 'Transport Started', icon: '🚚', color: 'yellow' },
+ 'transport.completed': { title: 'Transport Completed', icon: '✅', color: 'green' },
+ 'transport.verified': { title: 'Transport Verified', icon: '🔍', color: 'blue' },
+ 'demand.created': { title: 'Demand Created', icon: '📊', color: 'purple' },
+ 'demand.matched': { title: 'Demand Matched', icon: '🎯', color: 'green' },
+ 'supply.committed': { title: 'Supply Committed', icon: '📦', color: 'blue' },
+ 'farm.registered': { title: 'Farm Registered', icon: '🏭', color: 'green' },
+ 'farm.updated': { title: 'Farm Updated', icon: '🔧', color: 'gray' },
+ 'batch.started': { title: 'Batch Started', icon: '🌿', color: 'green' },
+ 'batch.harvested': { title: 'Batch Harvested', icon: '🥬', color: 'green' },
+ 'agent.alert': { title: 'Agent Alert', icon: '⚠️', color: 'yellow' },
+ 'agent.task_completed': { title: 'Task Completed', icon: '✔️', color: 'green' },
+ 'agent.error': { title: 'Agent Error', icon: '❌', color: 'red' },
+ 'blockchain.block_added': { title: 'Block Added', icon: '🔗', color: 'blue' },
+ 'blockchain.verified': { title: 'Blockchain Verified', icon: '✓', color: 'green' },
+ 'blockchain.error': { title: 'Blockchain Error', icon: '⛓️💥', color: 'red' },
+ 'system.health': { title: 'Health Check', icon: '💓', color: 'green' },
+ 'system.alert': { title: 'System Alert', icon: '🔔', color: 'yellow' },
+ 'system.metric': { title: 'Metric Update', icon: '📈', color: 'blue' },
+ 'audit.logged': { title: 'Audit Logged', icon: '📋', color: 'gray' },
+ 'audit.anomaly': { title: 'Anomaly Detected', icon: '🚨', color: 'red' },
+};
+
+/**
+ * Get the category for an event type
+ */
+export function getEventCategory(type: TransparencyEventType): EventCategory {
+ return EVENT_CATEGORIES[type];
+}
+
+/**
+ * Get display properties for an event type
+ */
+export function getEventDisplay(type: TransparencyEventType): EventDisplay {
+ return EVENT_DISPLAY[type] || { title: type, icon: '📌', color: 'gray' };
+}
+
+/**
+ * Format a description for an event
+ */
+export function formatEventDescription(event: TransparencyEvent): string {
+ const { type, data, source } = event;
+
+ switch (type) {
+ case 'plant.registered':
+ return `${data.name || 'A plant'} was registered by ${source}`;
+ case 'plant.cloned':
+ return `${data.name || 'A plant'} was cloned from ${data.parentName || 'parent'}`;
+ case 'plant.transferred':
+ return `${data.name || 'A plant'} was transferred to ${data.newOwner || 'new owner'}`;
+ case 'plant.updated':
+ return `${data.name || 'A plant'} information was updated`;
+ case 'transport.started':
+ return `Transport started from ${data.from || 'origin'} to ${data.to || 'destination'}`;
+ case 'transport.completed':
+ return `Transport completed: ${data.distance || '?'} km traveled`;
+ case 'transport.verified':
+ return `Transport verified on blockchain`;
+ case 'demand.created':
+ return `Demand signal created for ${data.product || 'product'}`;
+ case 'demand.matched':
+ return `Demand matched with ${data.supplier || 'a supplier'}`;
+ case 'supply.committed':
+ return `Supply committed: ${data.quantity || '?'} units`;
+ case 'farm.registered':
+ return `Vertical farm "${data.name || 'Farm'}" registered`;
+ case 'farm.updated':
+ return `Farm settings updated`;
+ case 'batch.started':
+ return `Growing batch started: ${data.cropType || 'crops'}`;
+ case 'batch.harvested':
+ return `Batch harvested: ${data.yield || '?'} kg`;
+ case 'agent.alert':
+ return data.message || 'Agent alert triggered';
+ case 'agent.task_completed':
+ return `Task completed: ${data.taskName || 'task'}`;
+ case 'agent.error':
+ return `Agent error: ${data.error || 'unknown error'}`;
+ case 'blockchain.block_added':
+ return `New block added to chain`;
+ case 'blockchain.verified':
+ return `Blockchain integrity verified`;
+ case 'blockchain.error':
+ return `Blockchain error: ${data.error || 'unknown error'}`;
+ case 'system.health':
+ return `System health: ${data.status || 'OK'}`;
+ case 'system.alert':
+ return data.message || 'System alert';
+ case 'system.metric':
+ return `Metric: ${data.metricName || 'metric'} = ${data.value || '?'}`;
+ case 'audit.logged':
+ return `Audit: ${data.action || 'action'} by ${data.actor || 'unknown'}`;
+ case 'audit.anomaly':
+ return `Anomaly: ${data.description || 'unusual activity detected'}`;
+ default:
+ return `Event from ${source}`;
+ }
+}
+
+/**
+ * Convert a TransparencyEvent to a LiveFeedItem
+ */
+export function toFeedItem(event: TransparencyEvent): LiveFeedItem {
+ const display = getEventDisplay(event.type);
+
+ return {
+ id: event.id,
+ event,
+ timestamp: new Date(event.timestamp).getTime(),
+ formatted: {
+ title: display.title,
+ description: formatEventDescription(event),
+ icon: display.icon,
+ color: display.color,
+ },
+ };
+}
+
+/**
+ * Get events by category
+ */
+export function getEventsByCategory(category: EventCategory): TransparencyEventType[] {
+ return Object.entries(EVENT_CATEGORIES)
+ .filter(([, cat]) => cat === category)
+ .map(([type]) => type as TransparencyEventType);
+}
+
+/**
+ * Check if an event type belongs to a category
+ */
+export function isEventInCategory(type: TransparencyEventType, category: EventCategory): boolean {
+ return EVENT_CATEGORIES[type] === category;
+}
+
+/**
+ * Priority to numeric value for sorting
+ */
+export function priorityToNumber(priority: string): number {
+ switch (priority) {
+ case 'CRITICAL': return 4;
+ case 'HIGH': return 3;
+ case 'NORMAL': return 2;
+ case 'LOW': return 1;
+ default: return 0;
+ }
+}
+
+/**
+ * Sort events by priority and time
+ */
+export function sortEvents(events: TransparencyEvent[]): TransparencyEvent[] {
+ return [...events].sort((a, b) => {
+ const priorityDiff = priorityToNumber(b.priority) - priorityToNumber(a.priority);
+ if (priorityDiff !== 0) return priorityDiff;
+ return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
+ });
+}
diff --git a/lib/realtime/index.ts b/lib/realtime/index.ts
new file mode 100644
index 0000000..8c631ec
--- /dev/null
+++ b/lib/realtime/index.ts
@@ -0,0 +1,92 @@
+/**
+ * Real-Time Module for LocalGreenChain
+ *
+ * Provides WebSocket-based real-time updates using Socket.io
+ * with automatic fallback to SSE.
+ */
+
+// Types
+export type {
+ RoomType,
+ ConnectionStatus,
+ SocketAuthPayload,
+ SocketHandshake,
+ ClientToServerEvents,
+ ServerToClientEvents,
+ InterServerEvents,
+ SocketData,
+ RealtimeNotification,
+ ConnectionMetrics,
+ LiveFeedItem,
+ TransparencyEventType,
+ EventPriority,
+ TransparencyEvent,
+} from './types';
+
+// Events
+export {
+ EventCategory,
+ RealtimeEvent,
+ EVENT_CATEGORIES,
+ EVENT_DISPLAY,
+ getEventCategory,
+ getEventDisplay,
+ formatEventDescription,
+ toFeedItem,
+ getEventsByCategory,
+ isEventInCategory,
+ priorityToNumber,
+ sortEvents,
+} from './events';
+
+// Rooms
+export {
+ parseRoom,
+ createRoom,
+ getDefaultRooms,
+ getCategoryRoom,
+ getEventRooms,
+ isValidRoom,
+ canJoinRoom,
+ ROOM_LIMITS,
+} from './rooms';
+
+// Server
+export {
+ RealtimeSocketServer,
+ getSocketServer,
+} from './socketServer';
+
+// Client
+export {
+ RealtimeSocketClient,
+ getSocketClient,
+ createSocketClient,
+} from './socketClient';
+export type {
+ SocketClientConfig,
+ EventListener,
+ StatusListener,
+ ErrorListener,
+} from './socketClient';
+
+// React Hooks
+export {
+ useSocket,
+ useLiveFeed,
+ usePlantUpdates,
+ useFarmUpdates,
+ useConnectionStatus,
+ useEventCount,
+} from './useSocket';
+export type {
+ UseSocketOptions,
+ UseSocketReturn,
+} from './useSocket';
+
+// React Context
+export {
+ SocketProvider,
+ useSocketContext,
+ useOptionalSocketContext,
+} from './SocketContext';
diff --git a/lib/realtime/rooms.ts b/lib/realtime/rooms.ts
new file mode 100644
index 0000000..960dd8e
--- /dev/null
+++ b/lib/realtime/rooms.ts
@@ -0,0 +1,137 @@
+/**
+ * Room Management for Socket.io
+ *
+ * Manages room subscriptions for targeted event delivery.
+ */
+
+import type { RoomType, TransparencyEventType } from './types';
+import { EventCategory, getEventCategory } from './events';
+
+/**
+ * Parse a room type to extract its components
+ */
+export function parseRoom(room: RoomType): { type: string; id?: string } {
+ if (room.includes(':')) {
+ const [type, id] = room.split(':');
+ return { type, id };
+ }
+ return { type: room };
+}
+
+/**
+ * Create a room name for a specific entity
+ */
+export function createRoom(type: 'plant' | 'farm' | 'user', id: string): RoomType {
+ return `${type}:${id}` as RoomType;
+}
+
+/**
+ * Get the default rooms for a user based on their role
+ */
+export function getDefaultRooms(userId?: string): RoomType[] {
+ const rooms: RoomType[] = ['global'];
+ if (userId) {
+ rooms.push(`user:${userId}` as RoomType);
+ }
+ return rooms;
+}
+
+/**
+ * Get category-based room for an event type
+ */
+export function getCategoryRoom(type: TransparencyEventType): RoomType {
+ const category = getEventCategory(type);
+
+ switch (category) {
+ case EventCategory.PLANT:
+ return 'plants';
+ case EventCategory.TRANSPORT:
+ return 'transport';
+ case EventCategory.FARM:
+ return 'farms';
+ case EventCategory.DEMAND:
+ return 'demand';
+ case EventCategory.SYSTEM:
+ case EventCategory.AGENT:
+ case EventCategory.BLOCKCHAIN:
+ case EventCategory.AUDIT:
+ return 'system';
+ default:
+ return 'global';
+ }
+}
+
+/**
+ * Determine which rooms should receive an event
+ */
+export function getEventRooms(
+ type: TransparencyEventType,
+ data: Record
+): RoomType[] {
+ const rooms: RoomType[] = ['global'];
+
+ // Add category room
+ const categoryRoom = getCategoryRoom(type);
+ if (categoryRoom !== 'global') {
+ rooms.push(categoryRoom);
+ }
+
+ // Add entity-specific rooms
+ if (data.plantId && typeof data.plantId === 'string') {
+ rooms.push(`plant:${data.plantId}` as RoomType);
+ }
+ if (data.farmId && typeof data.farmId === 'string') {
+ rooms.push(`farm:${data.farmId}` as RoomType);
+ }
+ if (data.userId && typeof data.userId === 'string') {
+ rooms.push(`user:${data.userId}` as RoomType);
+ }
+
+ return rooms;
+}
+
+/**
+ * Check if a room is valid
+ */
+export function isValidRoom(room: string): room is RoomType {
+ const validPrefixes = ['global', 'plants', 'transport', 'farms', 'demand', 'system'];
+ const validEntityPrefixes = ['plant:', 'farm:', 'user:'];
+
+ if (validPrefixes.includes(room)) {
+ return true;
+ }
+
+ return validEntityPrefixes.some((prefix) => room.startsWith(prefix));
+}
+
+/**
+ * Room subscription limits per connection
+ */
+export const ROOM_LIMITS = {
+ maxRooms: 50,
+ maxEntityRooms: 20,
+ maxGlobalRooms: 10,
+};
+
+/**
+ * Check if a connection can join another room
+ */
+export function canJoinRoom(currentRooms: RoomType[], newRoom: RoomType): boolean {
+ if (currentRooms.length >= ROOM_LIMITS.maxRooms) {
+ return false;
+ }
+
+ const parsed = parseRoom(newRoom);
+ const entityRooms = currentRooms.filter((r) => r.includes(':')).length;
+ const globalRooms = currentRooms.filter((r) => !r.includes(':')).length;
+
+ if (parsed.id && entityRooms >= ROOM_LIMITS.maxEntityRooms) {
+ return false;
+ }
+
+ if (!parsed.id && globalRooms >= ROOM_LIMITS.maxGlobalRooms) {
+ return false;
+ }
+
+ return true;
+}
diff --git a/lib/realtime/socketClient.ts b/lib/realtime/socketClient.ts
new file mode 100644
index 0000000..21a38c8
--- /dev/null
+++ b/lib/realtime/socketClient.ts
@@ -0,0 +1,379 @@
+/**
+ * Socket.io Client for LocalGreenChain
+ *
+ * Provides a client-side wrapper for Socket.io connections.
+ */
+
+import { io, Socket } from 'socket.io-client';
+import type {
+ ClientToServerEvents,
+ ServerToClientEvents,
+ ConnectionStatus,
+ RoomType,
+ TransparencyEventType,
+ TransparencyEvent,
+ ConnectionMetrics,
+} from './types';
+
+type TypedSocket = Socket;
+
+/**
+ * Client configuration options
+ */
+export interface SocketClientConfig {
+ url?: string;
+ path?: string;
+ autoConnect?: boolean;
+ auth?: {
+ userId?: string;
+ token?: string;
+ sessionId?: string;
+ };
+ reconnection?: boolean;
+ reconnectionAttempts?: number;
+ reconnectionDelay?: number;
+}
+
+/**
+ * Event listener types
+ */
+export type EventListener = (event: TransparencyEvent) => void;
+export type StatusListener = (status: ConnectionStatus) => void;
+export type ErrorListener = (error: { code: string; message: string }) => void;
+
+/**
+ * Socket.io client wrapper
+ */
+class RealtimeSocketClient {
+ private socket: TypedSocket | null = null;
+ private config: SocketClientConfig;
+ private status: ConnectionStatus = 'disconnected';
+ private eventListeners: Set = new Set();
+ private statusListeners: Set = new Set();
+ private errorListeners: Set = new Set();
+ private metrics: ConnectionMetrics;
+ private pingInterval: NodeJS.Timeout | null = null;
+
+ constructor(config: SocketClientConfig = {}) {
+ this.config = {
+ url: typeof window !== 'undefined' ? window.location.origin : '',
+ path: '/api/socket',
+ autoConnect: true,
+ reconnection: true,
+ reconnectionAttempts: 10,
+ reconnectionDelay: 1000,
+ ...config,
+ };
+
+ this.metrics = {
+ status: 'disconnected',
+ eventsReceived: 0,
+ reconnectAttempts: 0,
+ rooms: [],
+ };
+ }
+
+ /**
+ * Connect to the server
+ */
+ connect(): void {
+ if (this.socket?.connected) {
+ return;
+ }
+
+ this.updateStatus('connecting');
+
+ this.socket = io(this.config.url!, {
+ path: this.config.path,
+ autoConnect: this.config.autoConnect,
+ auth: this.config.auth,
+ reconnection: this.config.reconnection,
+ reconnectionAttempts: this.config.reconnectionAttempts,
+ reconnectionDelay: this.config.reconnectionDelay,
+ transports: ['websocket', 'polling'],
+ });
+
+ this.setupEventHandlers();
+ }
+
+ /**
+ * Set up socket event handlers
+ */
+ private setupEventHandlers(): void {
+ if (!this.socket) return;
+
+ // Connection events
+ this.socket.on('connect', () => {
+ this.updateStatus('connected');
+ this.metrics.connectedAt = Date.now();
+ this.metrics.reconnectAttempts = 0;
+ this.startPingInterval();
+ });
+
+ this.socket.on('disconnect', () => {
+ this.updateStatus('disconnected');
+ this.stopPingInterval();
+ });
+
+ this.socket.on('connect_error', () => {
+ this.updateStatus('error');
+ this.metrics.reconnectAttempts++;
+ });
+
+ // Server events
+ this.socket.on('connection:established', (data) => {
+ console.log('[SocketClient] Connected:', data.socketId);
+ });
+
+ this.socket.on('connection:error', (error) => {
+ this.errorListeners.forEach((listener) => listener(error));
+ });
+
+ // Real-time events
+ this.socket.on('event', (event) => {
+ this.metrics.eventsReceived++;
+ this.metrics.lastEventAt = Date.now();
+ this.eventListeners.forEach((listener) => listener(event));
+ });
+
+ this.socket.on('event:batch', (events) => {
+ this.metrics.eventsReceived += events.length;
+ this.metrics.lastEventAt = Date.now();
+ events.forEach((event) => {
+ this.eventListeners.forEach((listener) => listener(event));
+ });
+ });
+
+ // Room events
+ this.socket.on('room:joined', (room) => {
+ if (!this.metrics.rooms.includes(room)) {
+ this.metrics.rooms.push(room);
+ }
+ });
+
+ this.socket.on('room:left', (room) => {
+ this.metrics.rooms = this.metrics.rooms.filter((r) => r !== room);
+ });
+
+ // System events
+ this.socket.on('system:message', (message) => {
+ console.log(`[SocketClient] System ${message.type}: ${message.text}`);
+ });
+
+ this.socket.on('system:heartbeat', () => {
+ // Heartbeat received - connection is alive
+ });
+
+ // Reconnection events
+ this.socket.io.on('reconnect_attempt', () => {
+ this.updateStatus('reconnecting');
+ this.metrics.reconnectAttempts++;
+ });
+
+ this.socket.io.on('reconnect', () => {
+ this.updateStatus('connected');
+ });
+ }
+
+ /**
+ * Start ping interval for latency measurement
+ */
+ private startPingInterval(): void {
+ this.stopPingInterval();
+
+ this.pingInterval = setInterval(() => {
+ if (this.socket?.connected) {
+ const start = Date.now();
+ this.socket.emit('ping', (serverTime) => {
+ this.metrics.latency = Date.now() - start;
+ });
+ }
+ }, 10000); // Every 10 seconds
+ }
+
+ /**
+ * Stop ping interval
+ */
+ private stopPingInterval(): void {
+ if (this.pingInterval) {
+ clearInterval(this.pingInterval);
+ this.pingInterval = null;
+ }
+ }
+
+ /**
+ * Update connection status and notify listeners
+ */
+ private updateStatus(status: ConnectionStatus): void {
+ this.status = status;
+ this.metrics.status = status;
+ this.statusListeners.forEach((listener) => listener(status));
+ }
+
+ /**
+ * Disconnect from the server
+ */
+ disconnect(): void {
+ this.stopPingInterval();
+ if (this.socket) {
+ this.socket.disconnect();
+ this.socket = null;
+ }
+ this.updateStatus('disconnected');
+ }
+
+ /**
+ * Join a room
+ */
+ joinRoom(room: RoomType): Promise {
+ return new Promise((resolve) => {
+ if (!this.socket?.connected) {
+ resolve(false);
+ return;
+ }
+
+ this.socket.emit('room:join', room, (success) => {
+ resolve(success);
+ });
+ });
+ }
+
+ /**
+ * Leave a room
+ */
+ leaveRoom(room: RoomType): Promise {
+ return new Promise((resolve) => {
+ if (!this.socket?.connected) {
+ resolve(false);
+ return;
+ }
+
+ this.socket.emit('room:leave', room, (success) => {
+ resolve(success);
+ });
+ });
+ }
+
+ /**
+ * Subscribe to specific event types
+ */
+ subscribeToTypes(types: TransparencyEventType[]): Promise {
+ return new Promise((resolve) => {
+ if (!this.socket?.connected) {
+ resolve(false);
+ return;
+ }
+
+ this.socket.emit('subscribe:types', types, (success) => {
+ resolve(success);
+ });
+ });
+ }
+
+ /**
+ * Unsubscribe from specific event types
+ */
+ unsubscribeFromTypes(types: TransparencyEventType[]): Promise {
+ return new Promise((resolve) => {
+ if (!this.socket?.connected) {
+ resolve(false);
+ return;
+ }
+
+ this.socket.emit('unsubscribe:types', types, (success) => {
+ resolve(success);
+ });
+ });
+ }
+
+ /**
+ * Get recent events
+ */
+ getRecentEvents(limit: number = 50): Promise {
+ return new Promise((resolve) => {
+ if (!this.socket?.connected) {
+ resolve([]);
+ return;
+ }
+
+ this.socket.emit('events:recent', limit, (events) => {
+ resolve(events);
+ });
+ });
+ }
+
+ /**
+ * Add an event listener
+ */
+ onEvent(listener: EventListener): () => void {
+ this.eventListeners.add(listener);
+ return () => this.eventListeners.delete(listener);
+ }
+
+ /**
+ * Add a status listener
+ */
+ onStatusChange(listener: StatusListener): () => void {
+ this.statusListeners.add(listener);
+ return () => this.statusListeners.delete(listener);
+ }
+
+ /**
+ * Add an error listener
+ */
+ onError(listener: ErrorListener): () => void {
+ this.errorListeners.add(listener);
+ return () => this.errorListeners.delete(listener);
+ }
+
+ /**
+ * Get current connection status
+ */
+ getStatus(): ConnectionStatus {
+ return this.status;
+ }
+
+ /**
+ * Get connection metrics
+ */
+ getMetrics(): ConnectionMetrics {
+ return { ...this.metrics };
+ }
+
+ /**
+ * Check if connected
+ */
+ isConnected(): boolean {
+ return this.socket?.connected ?? false;
+ }
+
+ /**
+ * Get socket ID
+ */
+ getSocketId(): string | undefined {
+ return this.socket?.id;
+ }
+}
+
+// Singleton instance for client-side use
+let clientInstance: RealtimeSocketClient | null = null;
+
+/**
+ * Get the singleton socket client instance
+ */
+export function getSocketClient(config?: SocketClientConfig): RealtimeSocketClient {
+ if (!clientInstance) {
+ clientInstance = new RealtimeSocketClient(config);
+ }
+ return clientInstance;
+}
+
+/**
+ * Create a new socket client instance
+ */
+export function createSocketClient(config?: SocketClientConfig): RealtimeSocketClient {
+ return new RealtimeSocketClient(config);
+}
+
+export { RealtimeSocketClient };
+export default RealtimeSocketClient;
diff --git a/lib/realtime/socketServer.ts b/lib/realtime/socketServer.ts
new file mode 100644
index 0000000..71d18a2
--- /dev/null
+++ b/lib/realtime/socketServer.ts
@@ -0,0 +1,343 @@
+/**
+ * Socket.io Server for LocalGreenChain
+ *
+ * Provides real-time WebSocket communication with automatic
+ * fallback to SSE for environments that don't support WebSockets.
+ */
+
+import { Server as SocketIOServer, Socket } from 'socket.io';
+import type { Server as HTTPServer } from 'http';
+import type {
+ ClientToServerEvents,
+ ServerToClientEvents,
+ InterServerEvents,
+ SocketData,
+ RoomType,
+ TransparencyEventType,
+ TransparencyEvent,
+} from './types';
+import { getEventStream } from '../transparency/EventStream';
+import { getEventRooms, isValidRoom, canJoinRoom, getDefaultRooms } from './rooms';
+import * as crypto from 'crypto';
+
+type TypedSocket = Socket;
+
+/**
+ * Socket.io server configuration
+ */
+interface SocketServerConfig {
+ cors?: {
+ origin: string | string[];
+ credentials?: boolean;
+ };
+ pingTimeout?: number;
+ pingInterval?: number;
+}
+
+/**
+ * Socket.io server wrapper for LocalGreenChain
+ */
+class RealtimeSocketServer {
+ private io: SocketIOServer | null = null;
+ private eventStreamSubscriptionId: string | null = null;
+ private connectedClients: Map = new Map();
+ private heartbeatInterval: NodeJS.Timeout | null = null;
+
+ /**
+ * Initialize the Socket.io server
+ */
+ initialize(httpServer: HTTPServer, config: SocketServerConfig = {}): void {
+ if (this.io) {
+ console.log('[RealtimeSocketServer] Already initialized');
+ return;
+ }
+
+ this.io = new SocketIOServer(httpServer, {
+ path: '/api/socket',
+ cors: config.cors || {
+ origin: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3001',
+ credentials: true,
+ },
+ pingTimeout: config.pingTimeout || 60000,
+ pingInterval: config.pingInterval || 25000,
+ transports: ['websocket', 'polling'],
+ });
+
+ this.setupMiddleware();
+ this.setupEventHandlers();
+ this.subscribeToEventStream();
+ this.startHeartbeat();
+
+ console.log('[RealtimeSocketServer] Initialized');
+ }
+
+ /**
+ * Set up authentication and rate limiting middleware
+ */
+ private setupMiddleware(): void {
+ if (!this.io) return;
+
+ // Authentication middleware
+ this.io.use((socket, next) => {
+ const auth = socket.handshake.auth;
+
+ // Generate session ID if not provided
+ const sessionId = auth.sessionId || `sess_${crypto.randomBytes(8).toString('hex')}`;
+
+ // Attach data to socket
+ socket.data = {
+ userId: auth.userId,
+ sessionId,
+ connectedAt: Date.now(),
+ rooms: new Set(),
+ subscribedTypes: new Set(),
+ };
+
+ next();
+ });
+ }
+
+ /**
+ * Set up socket event handlers
+ */
+ private setupEventHandlers(): void {
+ if (!this.io) return;
+
+ this.io.on('connection', (socket: TypedSocket) => {
+ console.log(`[RealtimeSocketServer] Client connected: ${socket.id}`);
+
+ // Store connected client
+ this.connectedClients.set(socket.id, socket);
+
+ // Join default rooms
+ const defaultRooms = getDefaultRooms(socket.data.userId);
+ defaultRooms.forEach((room) => {
+ socket.join(room);
+ socket.data.rooms.add(room);
+ });
+
+ // Send connection established event
+ socket.emit('connection:established', {
+ socketId: socket.id,
+ serverTime: Date.now(),
+ });
+
+ // Handle room join
+ socket.on('room:join', (room, callback) => {
+ if (!isValidRoom(room)) {
+ callback?.(false);
+ return;
+ }
+
+ if (!canJoinRoom(Array.from(socket.data.rooms), room)) {
+ callback?.(false);
+ return;
+ }
+
+ socket.join(room);
+ socket.data.rooms.add(room);
+ socket.emit('room:joined', room);
+ callback?.(true);
+ });
+
+ // Handle room leave
+ socket.on('room:leave', (room, callback) => {
+ socket.leave(room);
+ socket.data.rooms.delete(room);
+ socket.emit('room:left', room);
+ callback?.(true);
+ });
+
+ // Handle type subscriptions
+ socket.on('subscribe:types', (types, callback) => {
+ types.forEach((type) => socket.data.subscribedTypes.add(type));
+ callback?.(true);
+ });
+
+ // Handle type unsubscriptions
+ socket.on('unsubscribe:types', (types, callback) => {
+ types.forEach((type) => socket.data.subscribedTypes.delete(type));
+ callback?.(true);
+ });
+
+ // Handle ping for latency measurement
+ socket.on('ping', (callback) => {
+ callback(Date.now());
+ });
+
+ // Handle recent events request
+ socket.on('events:recent', (limit, callback) => {
+ const eventStream = getEventStream();
+ const events = eventStream.getRecent(Math.min(limit, 100));
+ callback(events);
+ });
+
+ // Handle disconnect
+ socket.on('disconnect', (reason) => {
+ console.log(`[RealtimeSocketServer] Client disconnected: ${socket.id} (${reason})`);
+ this.connectedClients.delete(socket.id);
+ });
+ });
+ }
+
+ /**
+ * Subscribe to the EventStream for broadcasting events
+ */
+ private subscribeToEventStream(): void {
+ const eventStream = getEventStream();
+
+ // Subscribe to all event types
+ this.eventStreamSubscriptionId = eventStream.subscribe(
+ eventStream.getAvailableEventTypes(),
+ (event) => {
+ this.broadcastEvent(event);
+ }
+ );
+
+ console.log('[RealtimeSocketServer] Subscribed to EventStream');
+ }
+
+ /**
+ * Broadcast an event to appropriate rooms
+ */
+ private broadcastEvent(event: TransparencyEvent): void {
+ if (!this.io) return;
+
+ // Get rooms that should receive this event
+ const rooms = getEventRooms(event.type, event.data);
+
+ // Emit to each room
+ rooms.forEach((room) => {
+ this.io!.to(room).emit('event', event);
+ });
+ }
+
+ /**
+ * Start heartbeat to keep connections alive
+ */
+ private startHeartbeat(): void {
+ if (this.heartbeatInterval) {
+ clearInterval(this.heartbeatInterval);
+ }
+
+ this.heartbeatInterval = setInterval(() => {
+ if (this.io) {
+ this.io.emit('system:heartbeat', Date.now());
+ }
+ }, 30000); // Every 30 seconds
+ }
+
+ /**
+ * Emit an event to specific rooms
+ */
+ emitToRooms(rooms: RoomType[], eventName: keyof ServerToClientEvents, data: unknown): void {
+ if (!this.io) return;
+
+ rooms.forEach((room) => {
+ (this.io!.to(room) as any).emit(eventName, data);
+ });
+ }
+
+ /**
+ * Emit an event to a specific user
+ */
+ emitToUser(userId: string, eventName: keyof ServerToClientEvents, data: unknown): void {
+ this.emitToRooms([`user:${userId}` as RoomType], eventName, data);
+ }
+
+ /**
+ * Emit a system message to all connected clients
+ */
+ emitSystemMessage(type: 'info' | 'warning' | 'error', text: string): void {
+ if (!this.io) return;
+
+ this.io.emit('system:message', { type, text });
+ }
+
+ /**
+ * Get connected client count
+ */
+ getConnectedCount(): number {
+ return this.connectedClients.size;
+ }
+
+ /**
+ * Get all connected socket IDs
+ */
+ getConnectedSockets(): string[] {
+ return Array.from(this.connectedClients.keys());
+ }
+
+ /**
+ * Get server stats
+ */
+ getStats(): {
+ connectedClients: number;
+ rooms: string[];
+ uptime: number;
+ } {
+ return {
+ connectedClients: this.connectedClients.size,
+ rooms: this.io ? Array.from(this.io.sockets.adapter.rooms.keys()) : [],
+ uptime: process.uptime(),
+ };
+ }
+
+ /**
+ * Shutdown the server gracefully
+ */
+ async shutdown(): Promise {
+ if (this.heartbeatInterval) {
+ clearInterval(this.heartbeatInterval);
+ }
+
+ if (this.eventStreamSubscriptionId) {
+ const eventStream = getEventStream();
+ eventStream.unsubscribe(this.eventStreamSubscriptionId);
+ }
+
+ if (this.io) {
+ // Notify all clients
+ this.io.emit('system:message', {
+ type: 'warning',
+ text: 'Server is shutting down',
+ });
+
+ // Disconnect all clients
+ this.io.disconnectSockets(true);
+
+ // Close server
+ await new Promise((resolve) => {
+ this.io!.close(() => {
+ console.log('[RealtimeSocketServer] Shutdown complete');
+ resolve();
+ });
+ });
+
+ this.io = null;
+ }
+ }
+
+ /**
+ * Get the Socket.io server instance
+ */
+ getIO(): SocketIOServer | null {
+ return this.io;
+ }
+}
+
+// Singleton instance
+let socketServerInstance: RealtimeSocketServer | null = null;
+
+/**
+ * Get the singleton socket server instance
+ */
+export function getSocketServer(): RealtimeSocketServer {
+ if (!socketServerInstance) {
+ socketServerInstance = new RealtimeSocketServer();
+ }
+ return socketServerInstance;
+}
+
+export { RealtimeSocketServer };
+export default RealtimeSocketServer;
diff --git a/lib/realtime/types.ts b/lib/realtime/types.ts
new file mode 100644
index 0000000..b313a82
--- /dev/null
+++ b/lib/realtime/types.ts
@@ -0,0 +1,152 @@
+/**
+ * Real-Time System Types for LocalGreenChain
+ *
+ * Defines all types used in the WebSocket/SSE real-time communication system.
+ */
+
+import type { TransparencyEventType, EventPriority, TransparencyEvent } from '../transparency/EventStream';
+
+// Re-export for convenience
+export type { TransparencyEventType, EventPriority, TransparencyEvent };
+
+/**
+ * Room types for Socket.io subscriptions
+ */
+export type RoomType =
+ | 'global' // All events
+ | 'plants' // All plant events
+ | 'transport' // All transport events
+ | 'farms' // All farm events
+ | 'demand' // All demand events
+ | 'system' // System events
+ | `plant:${string}` // Specific plant
+ | `farm:${string}` // Specific farm
+ | `user:${string}`; // User-specific events
+
+/**
+ * Connection status states
+ */
+export type ConnectionStatus =
+ | 'connecting'
+ | 'connected'
+ | 'disconnected'
+ | 'reconnecting'
+ | 'error';
+
+/**
+ * Socket authentication payload
+ */
+export interface SocketAuthPayload {
+ userId?: string;
+ token?: string;
+ sessionId?: string;
+}
+
+/**
+ * Socket handshake data
+ */
+export interface SocketHandshake {
+ auth: SocketAuthPayload;
+ rooms?: RoomType[];
+}
+
+/**
+ * Client-to-server events
+ */
+export interface ClientToServerEvents {
+ // Room management
+ 'room:join': (room: RoomType, callback?: (success: boolean) => void) => void;
+ 'room:leave': (room: RoomType, callback?: (success: boolean) => void) => void;
+
+ // Event subscriptions
+ 'subscribe:types': (types: TransparencyEventType[], callback?: (success: boolean) => void) => void;
+ 'unsubscribe:types': (types: TransparencyEventType[], callback?: (success: boolean) => void) => void;
+
+ // Ping for connection health
+ 'ping': (callback: (timestamp: number) => void) => void;
+
+ // Request recent events
+ 'events:recent': (limit: number, callback: (events: TransparencyEvent[]) => void) => void;
+}
+
+/**
+ * Server-to-client events
+ */
+export interface ServerToClientEvents {
+ // Real-time events
+ 'event': (event: TransparencyEvent) => void;
+ 'event:batch': (events: TransparencyEvent[]) => void;
+
+ // Connection status
+ 'connection:established': (data: { socketId: string; serverTime: number }) => void;
+ 'connection:error': (error: { code: string; message: string }) => void;
+
+ // Room notifications
+ 'room:joined': (room: RoomType) => void;
+ 'room:left': (room: RoomType) => void;
+
+ // System messages
+ 'system:message': (message: { type: 'info' | 'warning' | 'error'; text: string }) => void;
+ 'system:heartbeat': (timestamp: number) => void;
+}
+
+/**
+ * Inter-server events (for scaling)
+ */
+export interface InterServerEvents {
+ 'event:broadcast': (event: TransparencyEvent, rooms: RoomType[]) => void;
+}
+
+/**
+ * Socket data attached to each connection
+ */
+export interface SocketData {
+ userId?: string;
+ sessionId: string;
+ connectedAt: number;
+ rooms: Set;
+ subscribedTypes: Set;
+}
+
+/**
+ * Real-time notification for UI display
+ */
+export interface RealtimeNotification {
+ id: string;
+ type: 'success' | 'info' | 'warning' | 'error';
+ title: string;
+ message: string;
+ timestamp: number;
+ eventType?: TransparencyEventType;
+ data?: Record;
+ read: boolean;
+ dismissed: boolean;
+}
+
+/**
+ * Connection metrics for monitoring
+ */
+export interface ConnectionMetrics {
+ status: ConnectionStatus;
+ connectedAt?: number;
+ lastEventAt?: number;
+ eventsReceived: number;
+ reconnectAttempts: number;
+ latency?: number;
+ rooms: RoomType[];
+}
+
+/**
+ * Live feed item for display
+ */
+export interface LiveFeedItem {
+ id: string;
+ event: TransparencyEvent;
+ timestamp: number;
+ formatted: {
+ title: string;
+ description: string;
+ icon: string;
+ color: string;
+ };
+}
diff --git a/lib/realtime/useSocket.ts b/lib/realtime/useSocket.ts
new file mode 100644
index 0000000..ecd2f30
--- /dev/null
+++ b/lib/realtime/useSocket.ts
@@ -0,0 +1,258 @@
+/**
+ * React Hook for Socket.io Real-Time Updates
+ *
+ * Provides easy-to-use hooks for real-time data in React components.
+ */
+
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { getSocketClient, RealtimeSocketClient } from './socketClient';
+import type {
+ ConnectionStatus,
+ TransparencyEvent,
+ RoomType,
+ TransparencyEventType,
+ ConnectionMetrics,
+ LiveFeedItem,
+} from './types';
+import { toFeedItem } from './events';
+
+/**
+ * Hook configuration options
+ */
+export interface UseSocketOptions {
+ autoConnect?: boolean;
+ rooms?: RoomType[];
+ eventTypes?: TransparencyEventType[];
+ userId?: string;
+ maxEvents?: number;
+}
+
+/**
+ * Hook return type
+ */
+export interface UseSocketReturn {
+ status: ConnectionStatus;
+ isConnected: boolean;
+ events: TransparencyEvent[];
+ latestEvent: TransparencyEvent | null;
+ metrics: ConnectionMetrics;
+ connect: () => void;
+ disconnect: () => void;
+ joinRoom: (room: RoomType) => Promise;
+ leaveRoom: (room: RoomType) => Promise;
+ clearEvents: () => void;
+}
+
+/**
+ * Main socket hook for real-time updates
+ */
+export function useSocket(options: UseSocketOptions = {}): UseSocketReturn {
+ const {
+ autoConnect = true,
+ rooms = [],
+ eventTypes = [],
+ userId,
+ maxEvents = 100,
+ } = options;
+
+ const [status, setStatus] = useState('disconnected');
+ const [events, setEvents] = useState([]);
+ const [latestEvent, setLatestEvent] = useState(null);
+ const [metrics, setMetrics] = useState({
+ status: 'disconnected',
+ eventsReceived: 0,
+ reconnectAttempts: 0,
+ rooms: [],
+ });
+
+ const clientRef = useRef(null);
+ const cleanupRef = useRef<(() => void)[]>([]);
+
+ // Initialize client
+ useEffect(() => {
+ if (typeof window === 'undefined') return;
+
+ const client = getSocketClient({ auth: { userId } });
+ clientRef.current = client;
+
+ // Set up listeners
+ const unsubStatus = client.onStatusChange((newStatus) => {
+ setStatus(newStatus);
+ setMetrics(client.getMetrics());
+ });
+
+ const unsubEvent = client.onEvent((event) => {
+ setLatestEvent(event);
+ setEvents((prev) => {
+ const updated = [event, ...prev];
+ return updated.slice(0, maxEvents);
+ });
+ setMetrics(client.getMetrics());
+ });
+
+ cleanupRef.current = [unsubStatus, unsubEvent];
+
+ // Auto connect
+ if (autoConnect) {
+ client.connect();
+ }
+
+ // Initial metrics
+ setMetrics(client.getMetrics());
+
+ return () => {
+ cleanupRef.current.forEach((cleanup) => cleanup());
+ };
+ }, [autoConnect, userId, maxEvents]);
+
+ // Join initial rooms
+ useEffect(() => {
+ if (!clientRef.current || status !== 'connected') return;
+
+ rooms.forEach((room) => {
+ clientRef.current?.joinRoom(room);
+ });
+ }, [status, rooms]);
+
+ // Subscribe to event types
+ useEffect(() => {
+ if (!clientRef.current || status !== 'connected' || eventTypes.length === 0) return;
+
+ clientRef.current.subscribeToTypes(eventTypes);
+ }, [status, eventTypes]);
+
+ const connect = useCallback(() => {
+ clientRef.current?.connect();
+ }, []);
+
+ const disconnect = useCallback(() => {
+ clientRef.current?.disconnect();
+ }, []);
+
+ const joinRoom = useCallback(async (room: RoomType) => {
+ return clientRef.current?.joinRoom(room) ?? false;
+ }, []);
+
+ const leaveRoom = useCallback(async (room: RoomType) => {
+ return clientRef.current?.leaveRoom(room) ?? false;
+ }, []);
+
+ const clearEvents = useCallback(() => {
+ setEvents([]);
+ setLatestEvent(null);
+ }, []);
+
+ return {
+ status,
+ isConnected: status === 'connected',
+ events,
+ latestEvent,
+ metrics,
+ connect,
+ disconnect,
+ joinRoom,
+ leaveRoom,
+ clearEvents,
+ };
+}
+
+/**
+ * Hook for live feed display
+ */
+export function useLiveFeed(options: UseSocketOptions = {}): {
+ items: LiveFeedItem[];
+ isConnected: boolean;
+ status: ConnectionStatus;
+ clearFeed: () => void;
+} {
+ const { events, isConnected, status, clearEvents } = useSocket(options);
+
+ const items = events.map((event) => toFeedItem(event));
+
+ return {
+ items,
+ isConnected,
+ status,
+ clearFeed: clearEvents,
+ };
+}
+
+/**
+ * Hook for tracking a specific plant's real-time updates
+ */
+export function usePlantUpdates(plantId: string): {
+ events: TransparencyEvent[];
+ isConnected: boolean;
+} {
+ return useSocket({
+ rooms: [`plant:${plantId}` as RoomType],
+ eventTypes: [
+ 'plant.registered',
+ 'plant.cloned',
+ 'plant.transferred',
+ 'plant.updated',
+ 'transport.started',
+ 'transport.completed',
+ ],
+ }) as { events: TransparencyEvent[]; isConnected: boolean };
+}
+
+/**
+ * Hook for tracking a specific farm's real-time updates
+ */
+export function useFarmUpdates(farmId: string): {
+ events: TransparencyEvent[];
+ isConnected: boolean;
+} {
+ return useSocket({
+ rooms: [`farm:${farmId}` as RoomType],
+ eventTypes: [
+ 'farm.registered',
+ 'farm.updated',
+ 'batch.started',
+ 'batch.harvested',
+ 'agent.alert',
+ ],
+ }) as { events: TransparencyEvent[]; isConnected: boolean };
+}
+
+/**
+ * Hook for connection status only (lightweight)
+ */
+export function useConnectionStatus(): {
+ status: ConnectionStatus;
+ isConnected: boolean;
+ latency: number | undefined;
+} {
+ const { status, isConnected, metrics } = useSocket({ autoConnect: true });
+
+ return {
+ status,
+ isConnected,
+ latency: metrics.latency,
+ };
+}
+
+/**
+ * Hook for event counts (useful for notification badges)
+ */
+export function useEventCount(options: UseSocketOptions = {}): {
+ count: number;
+ unreadCount: number;
+ markAllRead: () => void;
+} {
+ const { events } = useSocket(options);
+ const [readCount, setReadCount] = useState(0);
+
+ const markAllRead = useCallback(() => {
+ setReadCount(events.length);
+ }, [events.length]);
+
+ return {
+ count: events.length,
+ unreadCount: Math.max(0, events.length - readCount),
+ markAllRead,
+ };
+}
+
+export default useSocket;
diff --git a/package.json b/package.json
index b1350a8..8d639be 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,8 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hook-form": "^7.8.6",
+ "socket.io": "^4.7.2",
+ "socket.io-client": "^4.7.2",
"socks-proxy-agent": "^8.0.2"
},
"devDependencies": {
diff --git a/pages/api/socket.ts b/pages/api/socket.ts
new file mode 100644
index 0000000..7a42de3
--- /dev/null
+++ b/pages/api/socket.ts
@@ -0,0 +1,78 @@
+/**
+ * Socket.io API Endpoint for LocalGreenChain
+ *
+ * This endpoint initializes the Socket.io server and handles
+ * the WebSocket upgrade for real-time communication.
+ */
+
+import type { NextApiRequest, NextApiResponse } from 'next';
+import type { Server as HTTPServer } from 'http';
+import type { Socket as NetSocket } from 'net';
+import { getSocketServer } from '../../lib/realtime/socketServer';
+
+/**
+ * Extended response type with socket server
+ */
+interface SocketResponse extends NextApiResponse {
+ socket: NetSocket & {
+ server: HTTPServer & {
+ io?: ReturnType['getIO'];
+ };
+ };
+}
+
+/**
+ * Socket.io initialization handler
+ *
+ * This endpoint is called when the Socket.io client connects.
+ * It initializes the Socket.io server if not already running.
+ */
+export default function handler(req: NextApiRequest, res: SocketResponse) {
+ // Only allow GET requests for WebSocket upgrade
+ if (req.method !== 'GET') {
+ res.status(405).json({ error: 'Method not allowed' });
+ return;
+ }
+
+ // Check if Socket.io is already initialized
+ if (res.socket.server.io) {
+ console.log('[Socket API] Socket.io already initialized');
+ res.status(200).json({ status: 'ok', message: 'Socket.io already running' });
+ return;
+ }
+
+ try {
+ // Get the socket server singleton
+ const socketServer = getSocketServer();
+
+ // Initialize with the HTTP server
+ socketServer.initialize(res.socket.server);
+
+ // Store reference on the server object
+ res.socket.server.io = socketServer.getIO();
+
+ console.log('[Socket API] Socket.io initialized successfully');
+
+ res.status(200).json({
+ status: 'ok',
+ message: 'Socket.io initialized',
+ stats: socketServer.getStats(),
+ });
+ } catch (error) {
+ console.error('[Socket API] Failed to initialize Socket.io:', error);
+ res.status(500).json({
+ status: 'error',
+ message: 'Failed to initialize Socket.io',
+ error: error instanceof Error ? error.message : 'Unknown error',
+ });
+ }
+}
+
+/**
+ * Disable body parsing for WebSocket upgrade
+ */
+export const config = {
+ api: {
+ bodyParser: false,
+ },
+};
diff --git a/pages/api/socket/stats.ts b/pages/api/socket/stats.ts
new file mode 100644
index 0000000..bae7751
--- /dev/null
+++ b/pages/api/socket/stats.ts
@@ -0,0 +1,63 @@
+/**
+ * Socket.io Stats API Endpoint
+ *
+ * Returns statistics about the WebSocket server and connections.
+ */
+
+import type { NextApiRequest, NextApiResponse } from 'next';
+import { getSocketServer } from '../../../lib/realtime/socketServer';
+import { getEventStream } from '../../../lib/transparency/EventStream';
+
+interface SocketStats {
+ server: {
+ connectedClients: number;
+ rooms: string[];
+ uptime: number;
+ };
+ events: {
+ totalEvents: number;
+ eventsLast24h: number;
+ eventsByType: Record;
+ averageEventsPerMinute: number;
+ };
+ status: 'running' | 'stopped';
+}
+
+/**
+ * Get Socket.io server statistics
+ */
+export default function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ if (req.method !== 'GET') {
+ res.status(405).json({ error: 'Method not allowed' });
+ return;
+ }
+
+ try {
+ const socketServer = getSocketServer();
+ const eventStream = getEventStream();
+
+ const serverStats = socketServer.getStats();
+ const eventStats = eventStream.getStats();
+
+ const stats: SocketStats = {
+ server: serverStats,
+ events: {
+ totalEvents: eventStats.totalEvents,
+ eventsLast24h: eventStats.eventsLast24h,
+ eventsByType: eventStats.eventsByType,
+ averageEventsPerMinute: eventStats.averageEventsPerMinute,
+ },
+ status: socketServer.getIO() ? 'running' : 'stopped',
+ };
+
+ res.status(200).json(stats);
+ } catch (error) {
+ console.error('[Socket Stats API] Error:', error);
+ res.status(500).json({
+ error: error instanceof Error ? error.message : 'Unknown error',
+ });
+ }
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index 8752b4d..2e16692 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -5,7 +5,23 @@ module.exports = {
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
- extend: {},
+ extend: {
+ animation: {
+ fadeIn: 'fadeIn 0.3s ease-in-out',
+ slideIn: 'slideIn 0.3s ease-out',
+ pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
+ },
+ keyframes: {
+ fadeIn: {
+ '0%': { opacity: '0', transform: 'translateY(-10px)' },
+ '100%': { opacity: '1', transform: 'translateY(0)' },
+ },
+ slideIn: {
+ '0%': { opacity: '0', transform: 'translateX(20px)' },
+ '100%': { opacity: '1', transform: 'translateX(0)' },
+ },
+ },
+ },
},
variants: {
extend: {},