import { useState, useEffect } from 'react'; import { Thermometer, Droplets, Wind, Sun, AlertTriangle, Activity, Settings, RefreshCw, ChevronRight, Wifi, WifiOff, Bell } from 'lucide-react'; import api from '../lib/api'; import { PageHeader, MetricCard, EmptyState, CardSkeleton } from '../components/ui/LinearPrimitives'; import { useNotifications } from '../hooks/useNotifications'; import { PulseSensorCard } from '../components/dashboard/PulseSensorCard'; interface SensorData { id: string; name: string; type: string; roomId?: string; latestReading?: { value: number; unit: string; timestamp: string; }; minThreshold?: number; maxThreshold?: number; alertCount: number; } interface PulseReading { deviceId: string; deviceName: string; temperature: number; humidity: number; vpd: number; dewpoint: number; light?: number; co2?: number; timestamp: Date; } interface EnvironmentAlert { id: string; type: string; severity: string; message: string; value?: number; createdAt: string; sensor?: { name: string }; } interface DashboardData { sensors: number; readings: Record; averages: Record; alerts: { active: number; list: EnvironmentAlert[]; }; profile?: any; } const sensorIcons: Record = { TEMPERATURE: Thermometer, HUMIDITY: Droplets, CO2: Wind, LIGHT_PAR: Sun, LIGHT_LUX: Sun, VPD: Activity, }; const sensorAccents: Record = { TEMPERATURE: 'warning', HUMIDITY: 'accent', CO2: 'accent', LIGHT_PAR: 'warning', LIGHT_LUX: 'warning', VPD: 'success', }; export default function EnvironmentDashboard() { const [dashboard, setDashboard] = useState(null); const [sensors, setSensors] = useState([]); const [pulseReadings, setPulseReadings] = useState([]); const [sparklines, setSparklines] = useState>({}); const [pulseConnected, setPulseConnected] = useState(false); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [selectedRoom, setSelectedRoom] = useState(''); const { connected: wsConnected, alerts: wsAlerts, unreadCount } = useNotifications(); useEffect(() => { loadData(); const interval = setInterval(loadData, 30000); // Refresh every 30s return () => clearInterval(interval); }, [selectedRoom]); const loadData = async () => { try { // Load internal sensors and dashboard const [dashRes, sensorsRes] = await Promise.all([ api.get('/environment/dashboard', { params: { roomId: selectedRoom || undefined } }), api.get('/environment/sensors', { params: { roomId: selectedRoom || undefined } }) ]); setDashboard(dashRes.data); setSensors(sensorsRes.data); // Try to load Pulse data try { const pulseStatusRes = await api.get('/pulse/status'); setPulseConnected(pulseStatusRes.data.connected); if (pulseStatusRes.data.connected) { // Fetch readings and sparklines in parallel // Use catch for sparklines to avoid failing the whole dash if rate limited const [readingsRes, sparklinesRes] = await Promise.all([ api.get('/pulse/readings'), api.get('/pulse/sparklines').catch(() => ({ data: { sparklines: {} } })) ]); setPulseReadings(readingsRes.data.readings || []); setSparklines(sparklinesRes.data.sparklines || {}); } } catch (pulseError) { console.log('Pulse not configured or error:', pulseError); setPulseConnected(false); } } catch (error) { console.error('Failed to load environment data:', error); } finally { setLoading(false); } }; const handleRefresh = async () => { setRefreshing(true); await loadData(); setRefreshing(false); }; const getStatusClass = (value: number, min?: number, max?: number): string => { if (min && value < min) return 'text-accent'; if (max && value > max) return 'text-destructive'; return 'text-success'; }; if (loading) { return (
{Array.from({ length: 4 }).map((_, i) => )}
); } const hasData = sensors.length > 0 || pulseReadings.length > 0; return (
{/* Live indicator */}
{wsConnected ? ( ) : ( )} {wsConnected ? 'Live' : 'Offline'} {unreadCount > 0 && ( {unreadCount} )}
} /> {/* Real-time Alerts from WebSocket */} {wsAlerts.length > 0 && (
{wsAlerts.length} Real-time Alert{wsAlerts.length > 1 ? 's' : ''}
{wsAlerts.slice(0, 3).map((alert, idx) => (

{alert.sensorName}: {alert.type}

Value: {alert.value?.toFixed(1)} (threshold: {alert.threshold?.toFixed(1)})

{new Date(alert.timestamp).toLocaleTimeString()}
))}
)} {/* Active Alerts from database */} {dashboard?.alerts.active && dashboard.alerts.active > 0 && (
{dashboard.alerts.active} Active Alert{dashboard.alerts.active > 1 ? 's' : ''}
{dashboard.alerts.list.slice(0, 3).map(alert => (

{alert.message}

{new Date(alert.createdAt).toLocaleString()}

{alert.severity}
))}
)} {/* Pulse Sensors Section */} {pulseReadings.length > 0 && (

Pulse Grow Sensors

{pulseReadings.length} device{pulseReadings.length !== 1 ? 's' : ''}
{pulseReadings.map((reading) => ( r.temperature) }} /> ))}
)} {/* Quick Stats Grid (from internal sensors) */} {Object.keys(dashboard?.averages || {}).length > 0 && ( <>

Internal Sensors

{Object.entries(dashboard?.averages || {}).map(([type, value]) => { const Icon = sensorIcons[type] || Activity; const readings = dashboard?.readings[type] || []; const unit = readings[0]?.unit || ''; const accent = sensorAccents[type] || 'accent'; return ( ); })}
)} {/* Internal Sensors List */} {sensors.length > 0 && (

All Internal Sensors

{sensors.map(sensor => { const Icon = sensorIcons[sensor.type] || Activity; const reading = sensor.latestReading; return (
{sensor.name}
{sensor.type.replace('_', ' ').toLowerCase()}
{reading ? ( <>
{reading.value.toFixed(1)} {reading.unit}
{new Date(reading.timestamp).toLocaleTimeString()}
) : ( No data )}
{sensor.alertCount > 0 && ( {sensor.alertCount} )}
); })}
)} {/* Empty State - only show if no data at all */} {!hasData && !pulseConnected && (
)} {/* Environment Profile */} {dashboard?.profile && (
Active Profile: {dashboard.profile.name}
{dashboard.profile.tempMinF && dashboard.profile.tempMaxF && (
Temperature
{dashboard.profile.tempMinF}°F - {dashboard.profile.tempMaxF}°F
)} {dashboard.profile.humidityMin && dashboard.profile.humidityMax && (
Humidity
{dashboard.profile.humidityMin}% - {dashboard.profile.humidityMax}%
)} {dashboard.profile.co2Min && dashboard.profile.co2Max && (
CO₂
{dashboard.profile.co2Min} - {dashboard.profile.co2Max} ppm
)} {dashboard.profile.lightHours && (
Light Cycle
{dashboard.profile.lightHours}h on
)}
)} ); }