+
+ {/* Temperature */}
+
+
- )}
+
+ {reading.temperature.toFixed(1)}°
+
+
- {isOffline && (
-
-
Last Updated
-
- {readingTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
-
+ {/* Humidity */}
+
+
+
+ RH
- )}
+
+ {reading.humidity.toFixed(0)}%
+
+
+
+ {/* VPD */}
+
+
+
+ VPD
+
+
+ {reading.vpd.toFixed(2)}
+
+
+
+
+ {/* Dewpoint Row */}
+
+
+
+ Dewpoint: {reading.dewpoint.toFixed(1)}°F
+
+
+ View Details
+
+
+
+
+
+ {/* Timestamp Footer */}
+
+
+ Last reading
+
+ {readingTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+
diff --git a/frontend/src/pages/PulseTestPage.tsx b/frontend/src/pages/PulseTestPage.tsx
index 562564f..c2eff4e 100644
--- a/frontend/src/pages/PulseTestPage.tsx
+++ b/frontend/src/pages/PulseTestPage.tsx
@@ -1,6 +1,8 @@
import React, { useEffect, useState } from 'react';
-import { Activity, Thermometer, Droplets, Wind, Sun, Wifi, WifiOff, RefreshCw, Bell } from 'lucide-react';
+import { Activity, Thermometer, Droplets, Wind, Wifi, WifiOff, RefreshCw, Bell, TrendingUp, Clock, ChevronRight } from 'lucide-react';
+import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, AreaChart, ReferenceLine } from 'recharts';
import { useNotifications } from '../hooks/useNotifications';
+import { motion } from 'framer-motion';
import api from '../lib/api';
interface PulseDevice {
@@ -28,15 +30,25 @@ interface PulseStatus {
error?: string;
}
+interface HistoryPoint {
+ timestamp: string;
+ temperature: number;
+ humidity: number;
+ vpd: number;
+}
+
export default function PulseTestPage() {
const { connected: wsConnected, alerts, unreadCount } = useNotifications();
const [status, setStatus] = useState
(null);
const [devices, setDevices] = useState([]);
const [readings, setReadings] = useState([]);
+ const [history, setHistory] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [lastUpdate, setLastUpdate] = useState(null);
+ const [selectedDevice, setSelectedDevice] = useState(null);
+ const [historyHours, setHistoryHours] = useState(6);
const fetchData = async () => {
setLoading(true);
@@ -55,6 +67,11 @@ export default function PulseTestPage() {
// Fetch readings
const readingsRes = await api.get('/pulse/readings');
setReadings(readingsRes.data.readings || []);
+
+ // Set first device as selected if none selected
+ if (!selectedDevice && readingsRes.data.readings?.length > 0) {
+ setSelectedDevice(readingsRes.data.readings[0].deviceId);
+ }
}
setLastUpdate(new Date());
@@ -65,240 +82,364 @@ export default function PulseTestPage() {
}
};
+ const fetchHistory = async () => {
+ if (!selectedDevice) return;
+
+ try {
+ const res = await api.get(`/pulse/devices/${selectedDevice}/history?hours=${historyHours}`);
+ const points = (res.data.readings || []).map((r: any) => ({
+ timestamp: new Date(r.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
+ temperature: r.temperature,
+ humidity: r.humidity,
+ vpd: r.vpd
+ }));
+ setHistory(points);
+ } catch (err) {
+ console.error('Failed to fetch history:', err);
+ }
+ };
+
useEffect(() => {
fetchData();
-
- // Refresh every 30 seconds
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, []);
+ useEffect(() => {
+ fetchHistory();
+ }, [selectedDevice, historyHours]);
+
const getStatusColor = (connected: boolean) =>
connected ? 'text-green-500' : 'text-red-500';
const getVpdColor = (vpd: number) => {
if (vpd < 0.8) return 'text-blue-500';
- if (vpd > 1.2) return 'text-red-500';
+ if (vpd > 1.2) return 'text-amber-500';
+ if (vpd > 1.6) return 'text-red-500';
return 'text-green-500';
};
const getTempColor = (temp: number) => {
if (temp < 65) return 'text-blue-500';
- if (temp > 82) return 'text-red-500';
+ if (temp > 82) return 'text-amber-500';
+ if (temp > 90) return 'text-red-500';
return 'text-green-500';
};
+ const currentReading = readings.find(r => r.deviceId === selectedDevice) || readings[0];
+
return (
-
- {/* Header */}
-
-
-
- Pulse Sensor Dashboard
-
-
- Live environment monitoring from Pulse Grow
-
-
-
- {/* WebSocket Status */}
-
- {wsConnected ? (
-
- ) : (
-
- )}
-
- {wsConnected ? 'Live' : 'Offline'}
-
- {unreadCount > 0 && (
-
-
- {unreadCount}
-
- )}
-
-
- {/* Refresh Button */}
-
-
-
-
- {/* Connection Status Card */}
-
+
+
+ {/* Header */}
-
-
-
-
- Pulse API Connection
-
-
- {status?.connected
- ? `Connected • ${status.deviceCount} device(s)`
- : status?.error || 'Not connected'}
-
-
-
- {lastUpdate && (
-
- Last update: {lastUpdate.toLocaleTimeString()}
+
+
+
+ Pulse Sensor Analytics
+
+
+ Real-time environmental monitoring with historical trends
- )}
-
-
-
- {/* Error Message */}
- {error && (
-
- {error}
-
- )}
-
- {/* Sensor Readings Grid */}
- {readings.length > 0 && (
-
- {readings.map((reading, index) => (
-
- {/* Device Header */}
-
-
- {reading.deviceName || `Device ${reading.deviceId}`}
-
-
-
-
- {/* Metrics Grid */}
-
- {/* Temperature */}
-
-
-
- Temp
-
-
- {reading.temperature.toFixed(1)}°F
-
-
-
- {/* Humidity */}
-
-
-
- Humidity
-
-
- {reading.humidity.toFixed(1)}%
-
-
-
- {/* VPD */}
-
-
-
- VPD
-
-
- {reading.vpd.toFixed(2)} kPa
-
-
-
- {/* Dewpoint */}
-
-
-
- Dewpoint
-
-
- {reading.dewpoint.toFixed(1)}°F
-
-
-
- {/* Light (if available) */}
- {reading.light !== undefined && (
-
-
-
- Light
-
-
- {reading.light.toFixed(0)} lux
-
-
- )}
-
- {/* CO2 (if available) */}
- {reading.co2 !== undefined && (
-
-
-
- CO2
-
-
- {reading.co2} ppm
-
-
- )}
-
-
- {/* Timestamp */}
-
- {new Date(reading.timestamp).toLocaleString()}
-
+
+
+ {/* WebSocket Status */}
+
+ {wsConnected ? (
+
+ ) : (
+
+ )}
+
+ {wsConnected ? 'Live' : 'Offline'}
+
+ {unreadCount > 0 && (
+
+
+ {unreadCount}
+
+ )}
- ))}
-
- )}
- {/* Empty State */}
- {!loading && readings.length === 0 && status?.connected && (
-
-
-
- No Sensor Readings
-
-
- Waiting for data from Pulse devices...
-
-
- )}
-
- {/* Recent Alerts */}
- {alerts.length > 0 && (
-
-
-
- Recent Alerts ({alerts.length})
-
-
- {alerts.slice(0, 5).map((alert, index) => (
-
-
{alert.sensorName}
-
- {alert.type}: {alert.value.toFixed(1)} (threshold: {alert.threshold.toFixed(1)})
-
-
- {new Date(alert.timestamp).toLocaleTimeString()}
-
-
- ))}
+ {/* Refresh Button */}
+
- )}
+
+ {/* Error Message */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Main Content Grid */}
+
+ {/* Live Readings Panel */}
+
+
Live Sensors
+
+ {readings.map((reading) => (
+
setSelectedDevice(reading.deviceId)}
+ className={`p-4 rounded-xl cursor-pointer transition-all ${selectedDevice === reading.deviceId
+ ? 'bg-emerald-500/20 border-2 border-emerald-500/50'
+ : 'bg-slate-800/50 border border-slate-700/50 hover:border-slate-600'
+ }`}
+ >
+
+
{reading.deviceName}
+
+
+
+
+
+
Temp
+
+ {reading.temperature.toFixed(1)}°
+
+
+
+
RH
+
+ {reading.humidity.toFixed(0)}%
+
+
+
+
VPD
+
+ {reading.vpd.toFixed(2)}
+
+
+
+
+ ))}
+
+ {readings.length === 0 && !loading && (
+
+
+
No sensors connected
+
+ )}
+
+
+ {/* Charts Section */}
+
+ {/* Current Stats */}
+ {currentReading && (
+
+
+
+
+ Temperature
+
+
+ {currentReading.temperature.toFixed(1)}°F
+
+
+
+
+
+
+ Humidity
+
+
+ {currentReading.humidity.toFixed(0)}%
+
+
+
+
+
+
+ VPD
+
+
+ {currentReading.vpd.toFixed(2)}
+
+ kPa
+
+
+
+
+
+ Dewpoint
+
+
+ {currentReading.dewpoint.toFixed(1)}°F
+
+
+
+ )}
+
+ {/* Time Range Selector */}
+
+
+
+ Historical Trends
+
+
+
+
+
+
+
+ {/* Temperature Chart */}
+
+
+
+ Temperature History
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Humidity & VPD Charts */}
+
+
+
+
+ Humidity History
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ VPD History
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Recent Alerts */}
+ {alerts.length > 0 && (
+
+
+
+ Recent Alerts ({alerts.length})
+
+
+ {alerts.slice(0, 6).map((alert, index) => (
+
+
{alert.sensorName}
+
+ {alert.type}: {alert.value.toFixed(1)} (threshold: {alert.threshold.toFixed(1)})
+
+
+ {new Date(alert.timestamp).toLocaleTimeString()}
+
+
+ ))}
+
+
+ )}
+
);
}