feat: Pulse offline status indicator
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-05 22:52:33 -08:00
parent 1abb972d37
commit 22d0668ba1

View file

@ -24,6 +24,12 @@ interface PulseSensorCardProps {
export function PulseSensorCard({ reading, history, onClick }: PulseSensorCardProps) { export function PulseSensorCard({ reading, history, onClick }: PulseSensorCardProps) {
const navigate = useNavigate(); const navigate = useNavigate();
// Determine offline status (older than 15 mins)
const now = new Date();
const readingTime = new Date(reading.timestamp);
const diffMinutes = (now.getTime() - readingTime.getTime()) / 1000 / 60;
const isOffline = diffMinutes > 15;
// Determine status color based on VPD (gold standard for crop health) // Determine status color based on VPD (gold standard for crop health)
const getStatusColor = (vpd: number) => { const getStatusColor = (vpd: number) => {
if (vpd < 0.8 || vpd > 1.2) return 'text-amber-500'; // Warning if (vpd < 0.8 || vpd > 1.2) return 'text-amber-500'; // Warning
@ -31,7 +37,11 @@ export function PulseSensorCard({ reading, history, onClick }: PulseSensorCardPr
return 'text-emerald-500'; // Good return 'text-emerald-500'; // Good
}; };
const statusColor = getStatusColor(reading.vpd); // If offline, use neutral/error color for metrics
const statusColor = isOffline ? 'text-slate-400 dark:text-slate-500' : getStatusColor(reading.vpd);
const badgeColor = isOffline
? 'text-rose-500 bg-rose-500/10 border-rose-500/20'
: cn("bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700", statusColor);
// Simple Sparkline // Simple Sparkline
const Sparkline = ({ data, color = "#10b981", width = 120, height = 40 }: { data: number[], color?: string, width?: number, height?: number }) => { const Sparkline = ({ data, color = "#10b981", width = 120, height = 40 }: { data: number[], color?: string, width?: number, height?: number }) => {
@ -74,7 +84,12 @@ export function PulseSensorCard({ reading, history, onClick }: PulseSensorCardPr
> >
<div className="flex justify-between items-start mb-4"> <div className="flex justify-between items-start mb-4">
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<div className="p-2.5 rounded-xl bg-emerald-500/10 text-emerald-500 ring-1 ring-emerald-500/20"> <div className={cn(
"p-2.5 rounded-xl ring-1",
isOffline
? "bg-slate-100 text-slate-400 ring-slate-200 dark:bg-slate-800 dark:text-slate-500 dark:ring-slate-700"
: "bg-emerald-500/10 text-emerald-500 ring-emerald-500/20"
)}>
<Activity size={18} /> <Activity size={18} />
</div> </div>
<div> <div>
@ -85,8 +100,8 @@ export function PulseSensorCard({ reading, history, onClick }: PulseSensorCardPr
</div> </div>
</div> </div>
<div className={cn("text-[10px] font-bold px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700", statusColor)}> <div className={cn("text-[10px] font-bold px-2 py-0.5 rounded-full border", badgeColor)}>
{reading.timestamp ? 'LIVE' : 'OFFLINE'} {isOffline ? 'OFFLINE' : 'LIVE'}
</div> </div>
</div> </div>
@ -96,7 +111,7 @@ export function PulseSensorCard({ reading, history, onClick }: PulseSensorCardPr
<Thermometer size={12} /> <Thermometer size={12} />
<span className="text-[10px] font-bold uppercase tracking-wider">Temp</span> <span className="text-[10px] font-bold uppercase tracking-wider">Temp</span>
</div> </div>
<p className="text-2xl font-bold text-[var(--color-text-primary)]"> <p className={cn("text-2xl font-bold transition-colors", isOffline ? "text-slate-400" : "text-[var(--color-text-primary)]")}>
{reading.temperature.toFixed(1)}° {reading.temperature.toFixed(1)}°
</p> </p>
</div> </div>
@ -106,7 +121,7 @@ export function PulseSensorCard({ reading, history, onClick }: PulseSensorCardPr
<Droplets size={12} /> <Droplets size={12} />
<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="text-2xl font-bold text-[var(--color-text-primary)]"> <p className={cn("text-2xl font-bold transition-colors", isOffline ? "text-slate-400" : "text-[var(--color-text-primary)]")}>
{reading.humidity.toFixed(0)}% {reading.humidity.toFixed(0)}%
</p> </p>
</div> </div>
@ -116,17 +131,26 @@ export function PulseSensorCard({ reading, history, onClick }: PulseSensorCardPr
<CloudFog size={12} /> <CloudFog size={12} />
<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("text-2xl font-bold", statusColor)}> <p className={cn("text-2xl font-bold transition-colors", statusColor)}>
{reading.vpd.toFixed(2)} {reading.vpd.toFixed(2)}
</p> </p>
</div> </div>
<div className="flex items-end justify-end"> <div className="flex flex-col items-end justify-end">
{history && history.temperature && ( {history && history.temperature && !isOffline && (
<div className="mb-1"> <div className="mb-1">
<Sparkline data={history.temperature} color={reading.temperature > 80 ? '#f43f5e' : '#10b981'} /> <Sparkline data={history.temperature} color={reading.temperature > 80 ? '#f43f5e' : '#10b981'} />
</div> </div>
)} )}
{isOffline && (
<div className="text-right">
<p className="text-[10px] font-medium text-rose-500">Last Updated</p>
<p className="text-[10px] text-[var(--color-text-tertiary)]">
{readingTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</p>
</div>
)}
</div> </div>
</div> </div>
</motion.div> </motion.div>