feat: Improve Pulse analytics and Environment Report theming for light/dark mode
Some checks are pending
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run

This commit is contained in:
fullsizemalt 2026-01-06 12:07:41 -08:00
parent 64d7d56792
commit 41dcdce993
3 changed files with 129 additions and 130 deletions

View file

@ -78,9 +78,9 @@ export function PulseSensorCard({ reading, history, thresholds, onClick }: Pulse
onClick={handleClick} onClick={handleClick}
className={cn( className={cn(
"group relative overflow-hidden rounded-2xl cursor-pointer transition-all", "group relative overflow-hidden rounded-2xl cursor-pointer transition-all",
"bg-gradient-to-br from-slate-800/80 to-slate-900/80", "bg-white dark:bg-gradient-to-br dark:from-slate-800/80 dark:to-slate-900/80",
"border border-slate-700/50 hover:border-emerald-500/50", "border border-slate-200 dark:border-slate-700/50 hover:border-emerald-500/50",
"shadow-xl hover:shadow-2xl hover:shadow-emerald-500/10" "shadow-sm dark:shadow-xl hover:shadow-md dark:hover:shadow-2xl hover:shadow-emerald-500/10"
)} )}
> >
{/* Header */} {/* Header */}
@ -90,28 +90,28 @@ export function PulseSensorCard({ reading, history, thresholds, onClick }: Pulse
<div className={cn( <div className={cn(
"p-3 rounded-xl", "p-3 rounded-xl",
isOffline isOffline
? "bg-slate-700 text-slate-400" ? "bg-slate-100 dark:bg-slate-700 text-slate-500 dark:text-slate-400"
: isTempWarning || isVpdWarning : isTempWarning || isVpdWarning
? "bg-amber-500/20 text-amber-400" ? "bg-amber-100 dark:bg-amber-500/20 text-amber-600 dark:text-amber-400"
: "bg-emerald-500/20 text-emerald-400" : "bg-emerald-100 dark:bg-emerald-500/20 text-emerald-600 dark:text-emerald-400"
)}> )}>
<Activity size={22} /> <Activity size={22} />
</div> </div>
<div> <div>
<h3 className="text-sm font-bold text-white tracking-wide"> <h3 className="text-sm font-bold text-slate-900 dark:text-white tracking-wide">
{reading.deviceName} {reading.deviceName}
</h3> </h3>
<p className="text-xs text-slate-500">Pulse Grow Sensor</p> <p className="text-xs text-slate-500 dark:text-slate-500">Pulse Grow Sensor</p>
</div> </div>
</div> </div>
<div className={cn( <div className={cn(
"flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full border", "flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full border",
isOffline isOffline
? "text-red-400 bg-red-500/10 border-red-500/30" ? "text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-500/10 border-red-200 dark:border-red-500/30"
: "text-emerald-400 bg-emerald-500/10 border-emerald-500/30" : "text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/30"
)}> )}>
<div className={cn("w-1.5 h-1.5 rounded-full", isOffline ? "bg-red-400" : "bg-emerald-400 animate-pulse")} /> <div className={cn("w-1.5 h-1.5 rounded-full", isOffline ? "bg-red-500 dark:bg-red-400" : "bg-emerald-500 dark:bg-emerald-400 animate-pulse")} />
{isOffline ? 'OFFLINE' : 'LIVE'} {isOffline ? 'OFFLINE' : 'LIVE'}
</div> </div>
</div> </div>
@ -146,10 +146,10 @@ export function PulseSensorCard({ reading, history, thresholds, onClick }: Pulse
{/* Temperature */} {/* Temperature */}
<div className={cn( <div className={cn(
"p-3 rounded-xl transition-colors", "p-3 rounded-xl transition-colors",
isTempWarning ? "bg-amber-500/10" : "bg-slate-800/50" isTempWarning ? "bg-amber-50 dark:bg-amber-500/10" : "bg-slate-50 dark:bg-slate-800/50"
)}> )}>
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-1.5 text-slate-400"> <div className="flex items-center gap-1.5 text-slate-500 dark:text-slate-400">
<Thermometer size={14} /> <Thermometer size={14} />
<span className="text-[10px] font-bold uppercase tracking-wider">Temp</span> <span className="text-[10px] font-bold uppercase tracking-wider">Temp</span>
</div> </div>
@ -157,21 +157,21 @@ export function PulseSensorCard({ reading, history, thresholds, onClick }: Pulse
</div> </div>
<p className={cn( <p className={cn(
"text-2xl font-bold", "text-2xl font-bold",
isOffline ? "text-slate-500" : isTempWarning ? "text-amber-400" : "text-white" isOffline ? "text-slate-400 dark:text-slate-500" : isTempWarning ? "text-amber-600 dark:text-amber-400" : "text-slate-900 dark:text-white"
)}> )}>
{reading.temperature.toFixed(1)}° {reading.temperature.toFixed(1)}°
</p> </p>
</div> </div>
{/* Humidity */} {/* Humidity */}
<div className="p-3 rounded-xl bg-slate-800/50"> <div className="p-3 rounded-xl bg-slate-50 dark:bg-slate-800/50">
<div className="flex items-center gap-1.5 mb-1 text-slate-400"> <div className="flex items-center gap-1.5 mb-1 text-slate-500 dark:text-slate-400">
<Droplets size={14} /> <Droplets size={14} />
<span className="text-[10px] font-bold uppercase tracking-wider">RH</span> <span className="text-[10px] font-bold uppercase tracking-wider">RH</span>
</div> </div>
<p className={cn( <p className={cn(
"text-2xl font-bold", "text-2xl font-bold",
isOffline ? "text-slate-500" : "text-blue-400" isOffline ? "text-slate-400 dark:text-slate-500" : "text-blue-600 dark:text-blue-400"
)}> )}>
{reading.humidity.toFixed(0)}% {reading.humidity.toFixed(0)}%
</p> </p>
@ -180,15 +180,15 @@ export function PulseSensorCard({ reading, history, thresholds, onClick }: Pulse
{/* VPD */} {/* VPD */}
<div className={cn( <div className={cn(
"p-3 rounded-xl transition-colors", "p-3 rounded-xl transition-colors",
isVpdWarning ? "bg-purple-500/10" : "bg-slate-800/50" isVpdWarning ? "bg-purple-50 dark:bg-purple-500/10" : "bg-slate-50 dark:bg-slate-800/50"
)}> )}>
<div className="flex items-center gap-1.5 mb-1 text-slate-400"> <div className="flex items-center gap-1.5 mb-1 text-slate-500 dark:text-slate-400">
<CloudFog size={14} /> <CloudFog size={14} />
<span className="text-[10px] font-bold uppercase tracking-wider">VPD</span> <span className="text-[10px] font-bold uppercase tracking-wider">VPD</span>
</div> </div>
<p className={cn( <p className={cn(
"text-2xl font-bold", "text-2xl font-bold",
isOffline ? "text-slate-500" : isVpdWarning ? "text-purple-400" : "text-emerald-400" isOffline ? "text-slate-400 dark:text-slate-500" : isVpdWarning ? "text-purple-600 dark:text-purple-400" : "text-emerald-600 dark:text-emerald-400"
)}> )}>
{reading.vpd.toFixed(2)} {reading.vpd.toFixed(2)}
</p> </p>
@ -197,11 +197,11 @@ export function PulseSensorCard({ reading, history, thresholds, onClick }: Pulse
{/* Dewpoint Row */} {/* Dewpoint Row */}
<div className="mt-3 flex items-center justify-between px-1"> <div className="mt-3 flex items-center justify-between px-1">
<div className="flex items-center gap-2 text-slate-500"> <div className="flex items-center gap-2 text-slate-500 dark:text-slate-500">
<Wind size={12} /> <Wind size={12} />
<span className="text-xs">Dewpoint: <span className="text-slate-300">{reading.dewpoint.toFixed(1)}°F</span></span> <span className="text-xs">Dewpoint: <span className="text-slate-700 dark:text-slate-300">{reading.dewpoint.toFixed(1)}°F</span></span>
</div> </div>
<div className="flex items-center gap-1 text-slate-500 group-hover:text-emerald-400 transition-colors"> <div className="flex items-center gap-1 text-slate-400 dark:text-slate-500 group-hover:text-emerald-500 group-hover:dark:text-emerald-400 transition-colors">
<span className="text-xs font-medium">View Details</span> <span className="text-xs font-medium">View Details</span>
<ChevronRight size={14} className="group-hover:translate-x-0.5 transition-transform" /> <ChevronRight size={14} className="group-hover:translate-x-0.5 transition-transform" />
</div> </div>
@ -210,9 +210,9 @@ export function PulseSensorCard({ reading, history, thresholds, onClick }: Pulse
{/* Timestamp Footer */} {/* Timestamp Footer */}
<div className="px-5 pb-4"> <div className="px-5 pb-4">
<div className="flex items-center justify-between text-xs text-slate-500 pt-3 border-t border-slate-700/50"> <div className="flex items-center justify-between text-xs text-slate-400 dark:text-slate-500 pt-3 border-t border-slate-100 dark:border-slate-700/50">
<span>Last reading</span> <span>Last reading</span>
<span className={isOffline ? "text-red-400" : ""}> <span className={isOffline ? "text-red-500 dark:text-red-400" : ""}>
{readingTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} {readingTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span> </span>
</div> </div>

View file

@ -378,20 +378,21 @@ export default function EnvironmentReportPage() {
</div> </div>
{/* Summary Stats */} {/* Summary Stats */}
<div className="grid grid-cols-4 gap-4"> {/* Summary Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="p-5 rounded-2xl bg-white dark:bg-gradient-to-br dark:from-red-500/20 dark:to-orange-500/10 border border-red-200 dark:border-red-500/20 shadow-sm dark:shadow-none" className="p-5 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm dark:shadow-none"
> >
<div className="flex items-center gap-2 mb-2 text-slate-500 dark:text-slate-400"> <div className="flex items-center gap-2 mb-2 text-slate-500 dark:text-slate-400">
<Thermometer className="w-5 h-5 text-red-500 dark:text-red-400" /> <Thermometer className="w-5 h-5 text-red-500 dark:text-red-400" />
<span className="text-sm">Avg Temperature</span> <span className="text-sm font-medium">Avg Temperature</span>
</div> </div>
<p className="text-3xl font-bold text-slate-900 dark:text-white"> <p className="text-2xl md:text-3xl font-bold text-slate-900 dark:text-white">
{reportData?.devices[0]?.stats.temperature.avg.toFixed(1) || '--'}°F {reportData?.devices[0]?.stats.temperature.avg.toFixed(1) || '--'}°F
</p> </p>
<p className="text-xs text-slate-500 mt-1"> <p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
Range: {reportData?.devices[0]?.stats.temperature.min.toFixed(1)}° {reportData?.devices[0]?.stats.temperature.max.toFixed(1)}° Range: {reportData?.devices[0]?.stats.temperature.min.toFixed(1)}° {reportData?.devices[0]?.stats.temperature.max.toFixed(1)}°
</p> </p>
</motion.div> </motion.div>
@ -400,16 +401,16 @@ export default function EnvironmentReportPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }} transition={{ delay: 0.1 }}
className="p-5 rounded-2xl bg-white dark:bg-gradient-to-br dark:from-blue-500/20 dark:to-cyan-500/10 border border-blue-200 dark:border-blue-500/20 shadow-sm dark:shadow-none" className="p-5 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm dark:shadow-none"
> >
<div className="flex items-center gap-2 mb-2 text-slate-500 dark:text-slate-400"> <div className="flex items-center gap-2 mb-2 text-slate-500 dark:text-slate-400">
<Droplets className="w-5 h-5 text-blue-500 dark:text-blue-400" /> <Droplets className="w-5 h-5 text-blue-500 dark:text-blue-400" />
<span className="text-sm">Avg Humidity</span> <span className="text-sm font-medium">Avg Humidity</span>
</div> </div>
<p className="text-3xl font-bold text-slate-900 dark:text-white"> <p className="text-2xl md:text-3xl font-bold text-slate-900 dark:text-white">
{reportData?.devices[0]?.stats.humidity.avg.toFixed(0) || '--'}% {reportData?.devices[0]?.stats.humidity.avg.toFixed(0) || '--'}%
</p> </p>
<p className="text-xs text-slate-500 mt-1"> <p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
Range: {reportData?.devices[0]?.stats.humidity.min.toFixed(0)}% {reportData?.devices[0]?.stats.humidity.max.toFixed(0)}% Range: {reportData?.devices[0]?.stats.humidity.min.toFixed(0)}% {reportData?.devices[0]?.stats.humidity.max.toFixed(0)}%
</p> </p>
</motion.div> </motion.div>
@ -418,16 +419,16 @@ export default function EnvironmentReportPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }} transition={{ delay: 0.2 }}
className="p-5 rounded-2xl bg-white dark:bg-gradient-to-br dark:from-purple-500/20 dark:to-pink-500/10 border border-purple-200 dark:border-purple-500/20 shadow-sm dark:shadow-none" className="p-5 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm dark:shadow-none"
> >
<div className="flex items-center gap-2 mb-2 text-slate-500 dark:text-slate-400"> <div className="flex items-center gap-2 mb-2 text-slate-500 dark:text-slate-400">
<Wind className="w-5 h-5 text-purple-500 dark:text-purple-400" /> <Wind className="w-5 h-5 text-purple-500 dark:text-purple-400" />
<span className="text-sm">Avg VPD</span> <span className="text-sm font-medium">Avg VPD</span>
</div> </div>
<p className="text-3xl font-bold text-slate-900 dark:text-white"> <p className="text-2xl md:text-3xl font-bold text-slate-900 dark:text-white">
{reportData?.devices[0]?.stats.vpd.avg.toFixed(2) || '--'} kPa {reportData?.devices[0]?.stats.vpd.avg.toFixed(2) || '--'} kPa
</p> </p>
<p className="text-xs text-slate-500 mt-1"> <p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
Range: {reportData?.devices[0]?.stats.vpd.min.toFixed(2)} {reportData?.devices[0]?.stats.vpd.max.toFixed(2)} Range: {reportData?.devices[0]?.stats.vpd.min.toFixed(2)} {reportData?.devices[0]?.stats.vpd.max.toFixed(2)}
</p> </p>
</motion.div> </motion.div>
@ -436,16 +437,16 @@ export default function EnvironmentReportPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }} transition={{ delay: 0.3 }}
className="p-5 rounded-2xl bg-white dark:bg-gradient-to-br dark:from-amber-500/20 dark:to-yellow-500/10 border border-amber-200 dark:border-amber-500/20 shadow-sm dark:shadow-none" className="p-5 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm dark:shadow-none"
> >
<div className="flex items-center gap-2 mb-2 text-slate-500 dark:text-slate-400"> <div className="flex items-center gap-2 mb-2 text-slate-500 dark:text-slate-400">
<AlertTriangle className="w-5 h-5 text-amber-500 dark:text-amber-400" /> <AlertTriangle className="w-5 h-5 text-amber-500 dark:text-amber-400" />
<span className="text-sm">Alerts</span> <span className="text-sm font-medium">Alerts</span>
</div> </div>
<p className="text-3xl font-bold text-slate-900 dark:text-white"> <p className="text-2xl md:text-3xl font-bold text-slate-900 dark:text-white">
{reportData?.alerts.total || 0} {reportData?.alerts.total || 0}
</p> </p>
<p className="text-xs text-slate-500 mt-1"> <p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
<CheckCircle className="w-3 h-3 inline mr-1 text-green-500 dark:text-green-400" /> <CheckCircle className="w-3 h-3 inline mr-1 text-green-500 dark:text-green-400" />
{reportData?.alerts.resolved || 0} resolved {reportData?.alerts.resolved || 0} resolved
</p> </p>
@ -897,8 +898,8 @@ export default function EnvironmentReportPage() {
{/* Recent Alerts List */} {/* Recent Alerts List */}
{reportData?.alertAnalytics?.recentAlerts && reportData.alertAnalytics.recentAlerts.length > 0 ? ( {reportData?.alertAnalytics?.recentAlerts && reportData.alertAnalytics.recentAlerts.length > 0 ? (
<table className="w-full text-xs text-left border border-slate-200"> <table className="w-full text-xs text-left border border-slate-200 dark:border-slate-700">
<thead className="bg-slate-800 text-white font-semibold uppercase"> <thead className="bg-slate-800 text-white font-semibold uppercase dark:bg-slate-900">
<tr> <tr>
<th className="px-4 py-2">Time</th> <th className="px-4 py-2">Time</th>
<th className="px-4 py-2">Severity</th> <th className="px-4 py-2">Severity</th>
@ -907,23 +908,23 @@ export default function EnvironmentReportPage() {
<th className="px-4 py-2 text-right">Duration</th> <th className="px-4 py-2 text-right">Duration</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-200"> <tbody className="divide-y divide-slate-200 dark:divide-slate-700/50">
{reportData.alertAnalytics.recentAlerts.slice(0, 15).map((alert, idx) => ( {reportData.alertAnalytics.recentAlerts.slice(0, 15).map((alert, idx) => (
<tr key={alert.id} className={idx % 2 === 0 ? 'bg-white' : 'bg-slate-50'}> <tr key={alert.id} className={idx % 2 === 0 ? 'bg-white dark:bg-slate-800/50' : 'bg-slate-50 dark:bg-slate-800/30'}>
<td className="px-4 py-2 whitespace-nowrap text-slate-600"> <td className="px-4 py-2 whitespace-nowrap text-slate-600 dark:text-slate-400">
{new Date(alert.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' })} {new Date(alert.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' })}
</td> </td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<span className={`px-2 py-0.5 rounded-full font-bold text-[10px] ${alert.severity === 'CRITICAL' ? 'bg-red-100 text-red-800' : <span className={`px-2 py-0.5 rounded-full font-bold text-[10px] ${alert.severity === 'CRITICAL' ? 'bg-red-100 text-red-800 dark:bg-red-500/20 dark:text-red-300' :
alert.severity === 'WARNING' ? 'bg-amber-100 text-amber-800' : alert.severity === 'WARNING' ? 'bg-amber-100 text-amber-800 dark:bg-amber-500/20 dark:text-amber-300' :
'bg-blue-100 text-blue-800' 'bg-blue-100 text-blue-800 dark:bg-blue-500/20 dark:text-blue-300'
}`}> }`}>
{alert.severity} {alert.severity}
</span> </span>
</td> </td>
<td className="px-4 py-2 font-medium text-slate-900">{alert.type.replace('_', ' ')}</td> <td className="px-4 py-2 font-medium text-slate-900 dark:text-white">{alert.type.replace('_', ' ')}</td>
<td className="px-4 py-2 text-slate-600 truncate max-w-xs">{alert.message}</td> <td className="px-4 py-2 text-slate-600 dark:text-slate-400 truncate max-w-xs">{alert.message}</td>
<td className="px-4 py-2 text-right font-mono text-slate-700"> <td className="px-4 py-2 text-right font-mono text-slate-700 dark:text-slate-300">
{alert.resolutionTimeMin ? `${alert.resolutionTimeMin.toFixed(1)}m` : '-'} {alert.resolutionTimeMin ? `${alert.resolutionTimeMin.toFixed(1)}m` : '-'}
</td> </td>
</tr> </tr>
@ -931,7 +932,7 @@ export default function EnvironmentReportPage() {
</tbody> </tbody>
</table> </table>
) : ( ) : (
<p className="text-slate-500 italic text-sm p-4 border border-slate-200 rounded">No recent alerts found for this period.</p> <p className="text-slate-500 dark:text-slate-400 italic text-sm p-4 border border-slate-200 dark:border-slate-700 rounded">No recent alerts found for this period.</p>
)} )}
</div> </div>

View file

@ -129,30 +129,30 @@ export default function PulseTestPage() {
const currentReading = readings.find(r => r.deviceId === selectedDevice) || readings[0]; const currentReading = readings.find(r => r.deviceId === selectedDevice) || readings[0];
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 p-6"> <div className="min-h-screen bg-slate-50 dark:bg-slate-950 p-6 transition-colors duration-300">
<div className="max-w-7xl mx-auto space-y-6"> <div className="max-w-7xl mx-auto space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div> <div>
<h1 className="text-3xl font-bold text-white flex items-center gap-3"> <h1 className="text-3xl font-bold text-slate-900 dark:text-white flex items-center gap-3">
<div className="p-2.5 rounded-xl bg-gradient-to-br from-emerald-500 to-teal-600 shadow-lg shadow-emerald-500/25"> <div className="p-2.5 rounded-xl bg-gradient-to-br from-emerald-500 to-teal-600 shadow-lg shadow-emerald-500/25 text-white">
<Activity className="w-6 h-6 text-white" /> <Activity className="w-6 h-6" />
</div> </div>
Pulse Sensor Analytics Pulse Sensor Analytics
</h1> </h1>
<p className="text-slate-400 ml-14 mt-1"> <p className="text-slate-500 dark:text-slate-400 ml-14 mt-1">
Real-time environmental monitoring with historical trends Real-time environmental monitoring with historical trends
</p> </p>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{/* WebSocket Status */} {/* WebSocket Status */}
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-slate-800/50 border border-slate-700"> <div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 shadow-sm">
{wsConnected ? ( {wsConnected ? (
<Wifi className="w-4 h-4 text-green-500" /> <Wifi className="w-4 h-4 text-emerald-500" />
) : ( ) : (
<WifiOff className="w-4 h-4 text-red-500" /> <WifiOff className="w-4 h-4 text-red-500" />
)} )}
<span className="text-xs font-medium text-slate-300"> <span className="text-xs font-medium text-slate-600 dark:text-slate-300">
{wsConnected ? 'Live' : 'Offline'} {wsConnected ? 'Live' : 'Offline'}
</span> </span>
{unreadCount > 0 && ( {unreadCount > 0 && (
@ -167,16 +167,16 @@ export default function PulseTestPage() {
<button <button
onClick={fetchData} onClick={fetchData}
disabled={loading} disabled={loading}
className="p-2.5 rounded-xl bg-slate-800/50 border border-slate-700 hover:bg-slate-700 transition-colors disabled:opacity-50" className="p-2.5 rounded-xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors disabled:opacity-50 shadow-sm text-slate-500 dark:text-slate-400"
> >
<RefreshCw className={`w-5 h-5 text-slate-300 ${loading ? 'animate-spin' : ''}`} /> <RefreshCw className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} />
</button> </button>
</div> </div>
</div> </div>
{/* Error Message */} {/* Error Message */}
{error && ( {error && (
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/30 text-red-400"> <div className="p-4 rounded-xl bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/30 text-red-600 dark:text-red-400">
{error} {error}
</div> </div>
)} )}
@ -185,41 +185,41 @@ export default function PulseTestPage() {
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Live Readings Panel */} {/* Live Readings Panel */}
<div className="lg:col-span-1 space-y-4"> <div className="lg:col-span-1 space-y-4">
<h2 className="text-sm font-semibold text-slate-400 uppercase tracking-wider">Live Sensors</h2> <h2 className="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">Live Sensors</h2>
{readings.map((reading) => ( {readings.map((reading) => (
<motion.div <motion.div
key={reading.deviceId} key={reading.deviceId}
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}
onClick={() => setSelectedDevice(reading.deviceId)} onClick={() => setSelectedDevice(reading.deviceId)}
className={`p-4 rounded-xl cursor-pointer transition-all ${selectedDevice === reading.deviceId className={`p-4 rounded-xl cursor-pointer transition-all shadow-sm ${selectedDevice === reading.deviceId
? 'bg-emerald-500/20 border-2 border-emerald-500/50' ? 'bg-emerald-50 dark:bg-emerald-500/20 border-2 border-emerald-500/50'
: 'bg-slate-800/50 border border-slate-700/50 hover:border-slate-600' : 'bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 hover:border-slate-300 dark:hover:border-slate-600'
}`} }`}
> >
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-white text-sm">{reading.deviceName}</h3> <h3 className="font-semibold text-slate-900 dark:text-white text-sm">{reading.deviceName}</h3>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" /> <div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-xs text-green-400 font-medium">LIVE</span> <span className="text-xs text-emerald-600 dark:text-emerald-400 font-medium">LIVE</span>
</div> </div>
</div> </div>
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
<div> <div>
<p className="text-xs text-slate-500 mb-0.5">Temp</p> <p className="text-xs text-slate-500 dark:text-slate-400 mb-0.5">Temp</p>
<p className={`text-lg font-bold ${getTempColor(reading.temperature)}`}> <p className={`text-lg font-bold ${getTempColor(reading.temperature)}`}>
{reading.temperature.toFixed(1)}° {reading.temperature.toFixed(1)}°
</p> </p>
</div> </div>
<div> <div>
<p className="text-xs text-slate-500 mb-0.5">RH</p> <p className="text-xs text-slate-500 dark:text-slate-400 mb-0.5">RH</p>
<p className="text-lg font-bold text-blue-400"> <p className="text-lg font-bold text-blue-500 dark:text-blue-400">
{reading.humidity.toFixed(0)}% {reading.humidity.toFixed(0)}%
</p> </p>
</div> </div>
<div> <div>
<p className="text-xs text-slate-500 mb-0.5">VPD</p> <p className="text-xs text-slate-500 dark:text-slate-400 mb-0.5">VPD</p>
<p className={`text-lg font-bold ${getVpdColor(reading.vpd)}`}> <p className={`text-lg font-bold ${getVpdColor(reading.vpd)}`}>
{reading.vpd.toFixed(2)} {reading.vpd.toFixed(2)}
</p> </p>
@ -229,9 +229,9 @@ export default function PulseTestPage() {
))} ))}
{readings.length === 0 && !loading && ( {readings.length === 0 && !loading && (
<div className="p-6 text-center rounded-xl bg-slate-800/50 border border-slate-700/50"> <div className="p-6 text-center rounded-xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm">
<Activity className="w-8 h-8 mx-auto mb-2 text-slate-500" /> <Activity className="w-8 h-8 mx-auto mb-2 text-slate-400 dark:text-slate-500" />
<p className="text-slate-400 text-sm">No sensors connected</p> <p className="text-slate-500 dark:text-slate-400 text-sm">No sensors connected</p>
</div> </div>
)} )}
</div> </div>
@ -240,17 +240,17 @@ export default function PulseTestPage() {
<div className="lg:col-span-3 space-y-6"> <div className="lg:col-span-3 space-y-6">
{/* Current Stats */} {/* Current Stats */}
{currentReading && ( {currentReading && (
<div className="grid grid-cols-4 gap-4"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="p-5 rounded-2xl bg-gradient-to-br from-red-500/20 to-orange-500/10 border border-red-500/20" className="p-5 rounded-2xl bg-white dark:bg-slate-800/30 border border-red-100 dark:border-red-500/10 shadow-sm dark:shadow-none"
> >
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<Thermometer className="w-5 h-5 text-red-400" /> <Thermometer className="w-5 h-5 text-red-500 dark:text-red-400" />
<span className="text-sm text-slate-400">Temperature</span> <span className="text-sm text-slate-500 dark:text-slate-400">Temperature</span>
</div> </div>
<p className={`text-4xl font-bold ${getTempColor(currentReading.temperature)}`}> <p className={`text-3xl lg:text-4xl font-bold ${getTempColor(currentReading.temperature)}`}>
{currentReading.temperature.toFixed(1)}°F {currentReading.temperature.toFixed(1)}°F
</p> </p>
</motion.div> </motion.div>
@ -259,13 +259,13 @@ export default function PulseTestPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }} transition={{ delay: 0.1 }}
className="p-5 rounded-2xl bg-gradient-to-br from-blue-500/20 to-cyan-500/10 border border-blue-500/20" className="p-5 rounded-2xl bg-white dark:bg-slate-800/30 border border-blue-100 dark:border-blue-500/10 shadow-sm dark:shadow-none"
> >
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<Droplets className="w-5 h-5 text-blue-400" /> <Droplets className="w-5 h-5 text-blue-500 dark:text-blue-400" />
<span className="text-sm text-slate-400">Humidity</span> <span className="text-sm text-slate-500 dark:text-slate-400">Humidity</span>
</div> </div>
<p className="text-4xl font-bold text-blue-400"> <p className="text-3xl lg:text-4xl font-bold text-blue-500 dark:text-blue-400">
{currentReading.humidity.toFixed(0)}% {currentReading.humidity.toFixed(0)}%
</p> </p>
</motion.div> </motion.div>
@ -274,29 +274,29 @@ export default function PulseTestPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }} transition={{ delay: 0.2 }}
className="p-5 rounded-2xl bg-gradient-to-br from-purple-500/20 to-pink-500/10 border border-purple-500/20" className="p-5 rounded-2xl bg-white dark:bg-slate-800/30 border border-purple-100 dark:border-purple-500/10 shadow-sm dark:shadow-none"
> >
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<Wind className="w-5 h-5 text-purple-400" /> <Wind className="w-5 h-5 text-purple-500 dark:text-purple-400" />
<span className="text-sm text-slate-400">VPD</span> <span className="text-sm text-slate-500 dark:text-slate-400">VPD</span>
</div> </div>
<p className={`text-4xl font-bold ${getVpdColor(currentReading.vpd)}`}> <p className={`text-3xl lg:text-4xl font-bold ${getVpdColor(currentReading.vpd)}`}>
{currentReading.vpd.toFixed(2)} {currentReading.vpd.toFixed(2)}
</p> </p>
<p className="text-xs text-slate-500 mt-1">kPa</p> <p className="text-xs text-slate-500 dark:text-slate-500 mt-1">kPa</p>
</motion.div> </motion.div>
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }} transition={{ delay: 0.3 }}
className="p-5 rounded-2xl bg-gradient-to-br from-cyan-500/20 to-teal-500/10 border border-cyan-500/20" className="p-5 rounded-2xl bg-white dark:bg-slate-800/30 border border-cyan-100 dark:border-cyan-500/10 shadow-sm dark:shadow-none"
> >
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<Droplets className="w-5 h-5 text-cyan-400" /> <Droplets className="w-5 h-5 text-cyan-500 dark:text-cyan-400" />
<span className="text-sm text-slate-400">Dewpoint</span> <span className="text-sm text-slate-500 dark:text-slate-400">Dewpoint</span>
</div> </div>
<p className="text-4xl font-bold text-cyan-400"> <p className="text-3xl lg:text-4xl font-bold text-cyan-500 dark:text-cyan-400">
{currentReading.dewpoint.toFixed(1)}°F {currentReading.dewpoint.toFixed(1)}°F
</p> </p>
</motion.div> </motion.div>
@ -304,9 +304,9 @@ export default function PulseTestPage() {
)} )}
{/* Time Range Selector */} {/* Time Range Selector */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between pt-4">
<h2 className="text-lg font-semibold text-white flex items-center gap-2"> <h2 className="text-lg font-semibold text-slate-900 dark:text-white flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-emerald-400" /> <TrendingUp className="w-5 h-5 text-emerald-500 dark:text-emerald-400" />
Historical Trends Historical Trends
</h2> </h2>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -314,7 +314,7 @@ export default function PulseTestPage() {
<select <select
value={historyHours} value={historyHours}
onChange={(e) => setHistoryHours(Number(e.target.value))} onChange={(e) => setHistoryHours(Number(e.target.value))}
className="bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-sm text-white focus:outline-none focus:ring-2 focus:ring-emerald-500" className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg px-3 py-1.5 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 shadow-sm"
> >
<option value={1}>Last hour</option> <option value={1}>Last hour</option>
<option value={6}>Last 6 hours</option> <option value={6}>Last 6 hours</option>
@ -326,9 +326,9 @@ export default function PulseTestPage() {
</div> </div>
{/* Temperature Chart */} {/* Temperature Chart */}
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50"> <div className="p-6 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm">
<h3 className="text-sm font-medium text-slate-400 mb-4 flex items-center gap-2"> <h3 className="text-sm font-medium text-slate-500 dark:text-slate-400 mb-4 flex items-center gap-2">
<Thermometer className="w-4 h-4 text-red-400" /> <Thermometer className="w-4 h-4 text-red-500 dark:text-red-400" />
Temperature History Temperature History
</h3> </h3>
<div className="h-64"> <div className="h-64">
@ -340,12 +340,12 @@ export default function PulseTestPage() {
<stop offset="95%" stopColor="#ef4444" stopOpacity={0} /> <stop offset="95%" stopColor="#ef4444" stopOpacity={0} />
</linearGradient> </linearGradient>
</defs> </defs>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" /> <CartesianGrid strokeDasharray="3 3" stroke="#94a3b8" strokeOpacity={0.2} />
<XAxis dataKey="timestamp" stroke="#64748b" fontSize={11} /> <XAxis dataKey="timestamp" stroke="#94a3b8" fontSize={11} tickMargin={10} />
<YAxis stroke="#64748b" fontSize={11} domain={['auto', 'auto']} /> <YAxis stroke="#94a3b8" fontSize={11} domain={['auto', 'auto']} />
<Tooltip <Tooltip
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} contentStyle={{ backgroundColor: 'var(--color-bg-secondary)', borderColor: 'var(--color-border-subtle)', borderRadius: '8px', color: 'var(--color-text-primary)' }}
labelStyle={{ color: '#94a3b8' }} itemStyle={{ color: 'var(--color-text-primary)' }}
/> />
<ReferenceLine y={82} stroke="#f59e0b" strokeDasharray="5 5" /> <ReferenceLine y={82} stroke="#f59e0b" strokeDasharray="5 5" />
<ReferenceLine y={65} stroke="#3b82f6" strokeDasharray="5 5" /> <ReferenceLine y={65} stroke="#3b82f6" strokeDasharray="5 5" />
@ -356,10 +356,10 @@ export default function PulseTestPage() {
</div> </div>
{/* Humidity & VPD Charts */} {/* Humidity & VPD Charts */}
<div className="grid grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50"> <div className="p-6 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm">
<h3 className="text-sm font-medium text-slate-400 mb-4 flex items-center gap-2"> <h3 className="text-sm font-medium text-slate-500 dark:text-slate-400 mb-4 flex items-center gap-2">
<Droplets className="w-4 h-4 text-blue-400" /> <Droplets className="w-4 h-4 text-blue-500 dark:text-blue-400" />
Humidity History Humidity History
</h3> </h3>
<div className="h-48"> <div className="h-48">
@ -371,11 +371,11 @@ export default function PulseTestPage() {
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} /> <stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient> </linearGradient>
</defs> </defs>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" /> <CartesianGrid strokeDasharray="3 3" stroke="#94a3b8" strokeOpacity={0.2} />
<XAxis dataKey="timestamp" stroke="#64748b" fontSize={10} /> <XAxis dataKey="timestamp" stroke="#94a3b8" fontSize={10} tickMargin={10} />
<YAxis stroke="#64748b" fontSize={10} domain={[0, 100]} /> <YAxis stroke="#94a3b8" fontSize={10} domain={[0, 100]} />
<Tooltip <Tooltip
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} contentStyle={{ backgroundColor: 'var(--color-bg-secondary)', borderColor: 'var(--color-border-subtle)', borderRadius: '8px' }}
/> />
<Area type="monotone" dataKey="humidity" stroke="#3b82f6" fill="url(#humidityGradient)" strokeWidth={2} /> <Area type="monotone" dataKey="humidity" stroke="#3b82f6" fill="url(#humidityGradient)" strokeWidth={2} />
</AreaChart> </AreaChart>
@ -383,9 +383,9 @@ export default function PulseTestPage() {
</div> </div>
</div> </div>
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50"> <div className="p-6 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm">
<h3 className="text-sm font-medium text-slate-400 mb-4 flex items-center gap-2"> <h3 className="text-sm font-medium text-slate-500 dark:text-slate-400 mb-4 flex items-center gap-2">
<Wind className="w-4 h-4 text-purple-400" /> <Wind className="w-4 h-4 text-purple-500 dark:text-purple-400" />
VPD History VPD History
</h3> </h3>
<div className="h-48"> <div className="h-48">
@ -397,14 +397,12 @@ export default function PulseTestPage() {
<stop offset="95%" stopColor="#a855f7" stopOpacity={0} /> <stop offset="95%" stopColor="#a855f7" stopOpacity={0} />
</linearGradient> </linearGradient>
</defs> </defs>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" /> <CartesianGrid strokeDasharray="3 3" stroke="#94a3b8" strokeOpacity={0.2} />
<XAxis dataKey="timestamp" stroke="#64748b" fontSize={10} /> <XAxis dataKey="timestamp" stroke="#94a3b8" fontSize={10} tickMargin={10} />
<YAxis stroke="#64748b" fontSize={10} domain={[0, 2]} /> <YAxis stroke="#94a3b8" fontSize={10} domain={[0, 4]} />
<Tooltip <Tooltip
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} contentStyle={{ backgroundColor: 'var(--color-bg-secondary)', borderColor: 'var(--color-border-subtle)', borderRadius: '8px' }}
/> />
<ReferenceLine y={1.2} stroke="#f59e0b" strokeDasharray="3 3" />
<ReferenceLine y={0.8} stroke="#3b82f6" strokeDasharray="3 3" />
<Area type="monotone" dataKey="vpd" stroke="#a855f7" fill="url(#vpdGradient)" strokeWidth={2} /> <Area type="monotone" dataKey="vpd" stroke="#a855f7" fill="url(#vpdGradient)" strokeWidth={2} />
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
@ -416,9 +414,9 @@ export default function PulseTestPage() {
{/* Recent Alerts */} {/* Recent Alerts */}
{alerts.length > 0 && ( {alerts.length > 0 && (
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50"> <div className="p-6 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm">
<h3 className="font-semibold text-white mb-4 flex items-center gap-2"> <h3 className="font-semibold text-slate-900 dark:text-white mb-4 flex items-center gap-2">
<Bell className="w-5 h-5 text-amber-400" /> <Bell className="w-5 h-5 text-amber-500 dark:text-amber-400" />
Recent Alerts ({alerts.length}) Recent Alerts ({alerts.length})
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">