Implement Agent 6: Real-Time Updates feature for LocalGreenChain: - Add Socket.io server with room-based subscriptions - Create client-side hooks (useSocket, useLiveFeed, usePlantUpdates) - Add SocketProvider context for application-wide state - Implement UI components: - ConnectionStatus: Shows WebSocket connection state - LiveFeed: Real-time event feed display - NotificationToast: Toast notifications with auto-dismiss - LiveChart: Real-time data visualization - Add event type definitions and formatting utilities - Create socket API endpoint for WebSocket initialization - Add socket stats endpoint for monitoring - Extend tailwind with fadeIn/slideIn animations Integrates with existing EventStream SSE system for fallback.
167 lines
3.9 KiB
TypeScript
167 lines
3.9 KiB
TypeScript
/**
|
|
* 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 (
|
|
<div
|
|
className={classNames(
|
|
'inline-flex items-center gap-2',
|
|
className
|
|
)}
|
|
title={getStatusLabel(status)}
|
|
>
|
|
{/* Status dot */}
|
|
<span
|
|
className={classNames(
|
|
'rounded-full',
|
|
sizeClasses.dot,
|
|
getStatusColor(status)
|
|
)}
|
|
/>
|
|
|
|
{/* Label */}
|
|
{showLabel && (
|
|
<span className={classNames('text-gray-600', sizeClasses.text)}>
|
|
{getStatusLabel(status)}
|
|
</span>
|
|
)}
|
|
|
|
{/* Latency */}
|
|
{showLatency && status === 'connected' && latency !== undefined && (
|
|
<span className={classNames('text-gray-400', sizeClasses.text)}>
|
|
({latency}ms)
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Compact connection indicator (dot only)
|
|
*/
|
|
export function ConnectionDot({ className }: { className?: string }) {
|
|
const { status } = useConnectionStatus();
|
|
|
|
return (
|
|
<span
|
|
className={classNames(
|
|
'inline-block w-2 h-2 rounded-full',
|
|
getStatusColor(status),
|
|
className
|
|
)}
|
|
title={getStatusLabel(status)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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 (
|
|
<div className={bannerClasses}>
|
|
{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.'}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default ConnectionStatus;
|