"use client"; import { useEffect, useState, useCallback } from "react"; import { toast } from "sonner"; import { fetchLeakStatus, type LeakSensorStatus } from "@/lib/api"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { Droplets, RefreshCw, CheckCircle2, AlertTriangle, MapPin, Wind, Clock } from "lucide-react"; import { cn } from "@/lib/utils"; const SITE_ID = "sg-01"; function SensorBadge({ state }: { state: string }) { const cfg = { detected: { cls: "bg-destructive/10 text-destructive border-destructive/30", label: "LEAK DETECTED" }, clear: { cls: "bg-green-500/10 text-green-400 border-green-500/20", label: "Clear" }, unknown: { cls: "bg-muted/30 text-muted-foreground border-border", label: "Unknown" }, }[state] ?? { cls: "bg-muted/30 text-muted-foreground border-border", label: state }; return ( {cfg.label} ); } function SensorCard({ sensor }: { sensor: LeakSensorStatus }) { const detected = sensor.state === "detected"; const sensorAny = sensor as LeakSensorStatus & { last_triggered_at?: string | null; trigger_count_30d?: number }; const triggerCount30d = sensorAny.trigger_count_30d ?? 0; const lastTriggeredAt = sensorAny.last_triggered_at ?? null; let lastTriggeredText: string; if (lastTriggeredAt) { const daysAgo = Math.floor((Date.now() - new Date(lastTriggeredAt).getTime()) / (1000 * 60 * 60 * 24)); lastTriggeredText = daysAgo === 0 ? "Today" : `${daysAgo}d ago`; } else if (detected) { lastTriggeredText = "Currently active"; } else { lastTriggeredText = "No recent events"; } return (

{sensor.sensor_id}

{sensor.floor_zone && (

{sensor.floor_zone}

)}
{triggerCount30d} events (30d)
{sensor.room_id && (
Room: {sensor.room_id}
)} {sensor.near_crac && (
Near CRAC
)}
{sensor.under_floor ? "Under raised floor" : "Above floor level"}
{lastTriggeredText}
{detected && (
Water detected — inspect immediately and isolate if necessary
)}
); } export default function LeakDetectionPage() { const [sensors, setSensors] = useState([]); const [loading, setLoading] = useState(true); const load = useCallback(async () => { try { setSensors(await fetchLeakStatus(SITE_ID)); } catch { toast.error("Failed to load leak sensor data"); } finally { setLoading(false); } }, []); useEffect(() => { load(); const id = setInterval(load, 15_000); return () => clearInterval(id); }, [load]); const active = sensors.filter((s) => s.state === "detected"); const offline = sensors.filter((s) => s.state === "unknown"); const dry = sensors.filter((s) => s.state === "clear"); // Group by floor_zone const byZone = sensors.reduce>((acc, s) => { const zone = s.floor_zone ?? "Unassigned"; (acc[zone] ??= []).push(s); return acc; }, {}); const zoneEntries = Object.entries(byZone).sort(([a], [b]) => a.localeCompare(b)); return (
{/* Header */}

Leak Detection

Singapore DC01 — water sensor site map · refreshes every 15s

{!loading && ( 0 ? "bg-destructive/10 text-destructive" : "bg-green-500/10 text-green-400", )}> {active.length > 0 ? <> {active.length} leak{active.length > 1 ? "s" : ""} detected : <> No leaks detected} )}
{/* KPI bar */} {!loading && (
{[ { label: "Active Leaks", value: active.length, sub: "require immediate action", cls: active.length > 0 ? "border-destructive/40 bg-destructive/5 text-destructive" : "border-border bg-muted/10 text-green-400", }, { label: "Sensors Clear", value: dry.length, sub: `of ${sensors.length} total sensors`, cls: "border-border bg-muted/10 text-foreground", }, { label: "Offline", value: offline.length, sub: "no signal", cls: offline.length > 0 ? "border-amber-500/30 bg-amber-500/5 text-amber-400" : "border-border bg-muted/10 text-muted-foreground", }, ].map(({ label, value, sub, cls }) => (

{label}

{value}

{sub}

))}
)} {/* Active leak alert */} {!loading && active.length > 0 && (

{active.length} water leak{active.length > 1 ? "s" : ""} detected — immediate action required

{active.map((s) => (

{s.sensor_id} {s.floor_zone ? ` — ${s.floor_zone}` : ""} {s.near_crac ? ` (near ${s.near_crac})` : ""} {s.under_floor ? " — under raised floor" : ""}

))}
)} {/* Zone panels */} {loading ? (
{Array.from({ length: 4 }).map((_, i) => )}
) : ( zoneEntries.map(([zone, zoneSensors]) => { const zoneActive = zoneSensors.filter((s) => s.state === "detected"); return (

{zone}

{zoneActive.length > 0 && ( {zoneActive.length} LEAK )}
{zoneSensors.map((s) => )}
); }) )}
); }